Datavision / backend /graph /builder.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
6.63 kB
# Graph builder module
import networkx as nx
import re
import networkx as nx
import re
from config.settings import Settings
from graph.schema import detect_schema
from utils.paths import STORAGE_BASE
def detect_currency_from_value(value: str) -> str:
"""Detect currency from a value string like '$100' or 'β‚Ή1000'."""
if not isinstance(value, str):
return 'USD'
value = value.strip()
# Check multi-char symbols first
if value.startswith('A$') or 'AUD' in value.upper():
return 'AUD'
if value.startswith('C$') or 'CAD' in value.upper():
return 'CAD'
if value.startswith('S$') or 'SGD' in value.upper():
return 'SGD'
if value.startswith('CHF') or 'CHF' in value.upper():
return 'CHF'
# Single char symbols
if '€' in value:
return 'EUR'
if 'Β£' in value:
return 'GBP'
if 'β‚Ή' in value:
return 'INR'
if '$' in value and not any(x in value for x in ['A$', 'C$', 'S$']):
return 'USD'
if 'Β₯' in value:
return 'JPY' if '.' not in value else 'CNY'
return 'USD'
class GraphBuilder:
@staticmethod
def build(company_id: str, tables: list, default_currency: str = 'USD', source_files: list = None):
"""
Build knowledge graph from tabular data with multi-currency and source tracking.
Args:
company_id: Company identifier
tables: List of pandas DataFrames
default_currency: Default currency if none detected
source_files: List of source file names (one per table)
"""
G = nx.Graph()
currencies_detected = {} # Track currency counts
sources_tracked = {} # Track records per source file
for table_idx, df in enumerate(tables):
# Get source file name for this table
source_file = source_files[table_idx] if source_files and table_idx < len(source_files) else f"file_{table_idx}"
sources_tracked[source_file] = sources_tracked.get(source_file, 0)
schema = detect_schema(df.columns)
print(f"πŸ“Š Detected schema for {source_file}: {schema}")
print(f"πŸ“Š DataFrame columns: {list(df.columns)}")
print(f"πŸ“Š DataFrame shape: {df.shape}")
for idx, row in df.iterrows():
# Create invoice node with source file tracking
inv_id = f"invoice:{row[schema['invoice']]}_{source_file}" if "invoice" in schema else f"invoice:row_{idx}_{source_file}"
# Get amount and detect currency from the value
amount_raw = row.get(schema.get("amount"), 0.0)
currency = default_currency
try:
if isinstance(amount_raw, str):
# Detect currency from the string value
currency = detect_currency_from_value(amount_raw)
# Strip currency symbols and commas
amount = re.sub(r'[β‚Ή$€£Β₯,\s]', '', amount_raw)
amount = float(amount)
else:
amount = float(amount_raw)
except:
amount = 0.0
# Track currency frequency
currencies_detected[currency] = currencies_detected.get(currency, 0) + 1
sources_tracked[source_file] = sources_tracked.get(source_file, 0) + 1
# Store amount, currency AND source_file in the invoice node
G.add_node(inv_id, type="invoice", label=inv_id, amount=amount, currency=currency, source_file=source_file)
# Connect to customer (Fallback if missing)
if "customer" in schema:
val = str(row[schema["customer"]])
node_id = f"customer:{val}"
if not G.has_node(node_id):
G.add_node(node_id, type="customer", label=val)
G.add_edge(inv_id, node_id, relation="has_customer")
else:
# Fallback for missing customer
node_id = "customer:Unknown"
if not G.has_node(node_id):
G.add_node(node_id, type="customer", label="Unknown Customer")
G.add_edge(inv_id, node_id, relation="has_customer")
# Connect to product (Fallback if missing)
if "product" in schema:
val = str(row[schema["product"]])
node_id = f"product:{val}"
if not G.has_node(node_id):
G.add_node(node_id, type="product", label=val)
G.add_edge(inv_id, node_id, relation="has_product")
else:
# Fallback for missing product
node_id = "product:General"
if not G.has_node(node_id):
G.add_node(node_id, type="product", label="General Item")
G.add_edge(inv_id, node_id, relation="has_product")
# Connect to date (Optional)
if "date" in schema:
val = str(row[schema["date"]])
node_id = f"date:{val}"
if not G.has_node(node_id):
G.add_node(node_id, type="date", label=val)
G.add_edge(inv_id, node_id, relation="has_date")
print(f"πŸ“Š Built graph with {G.number_of_nodes()} nodes and {G.number_of_edges()} edges")
print(f"πŸ’° Currencies detected: {currencies_detected}")
# Save graph using pickle format to USER-SPECIFIC directory
import pickle
# Primary: User-specific graph directory (Consolidated storage)
user_graph_dir = STORAGE_BASE / company_id / "graph"
user_graph_dir.mkdir(parents=True, exist_ok=True)
user_graph_path = user_graph_dir / f"{company_id}.gpickle"
with open(user_graph_path, 'wb') as f:
pickle.dump(G, f)
print(f"βœ… Graph saved to user directory: {user_graph_path}")
# Also save to Settings.GRAPH_DIR for backward compatibility
output_path = Settings.GRAPH_DIR / f"{company_id}.gpickle"
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
pickle.dump(G, f)
print(f"βœ… Graph also saved to: {output_path}")
return G