Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions backend/app/ai/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
"record_payment": lambda p: p.get("amount", 0),
"refund_payment": lambda p: p.get("amount", 0),
"apply_discount": lambda p: p.get("discount_amount", 0),
"create_expense": lambda p: p.get("amount", 0),
"create_purchase_invoice": lambda p: sum(i.get("quantity", 0) * i.get("purchase_price", 0) for i in p.get("items", [])),
"set_customer_opening_balance": lambda p: p.get("amount", 0),
"set_supplier_opening_balance": lambda p: p.get("amount", 0),
"set_cash_opening_balance": lambda p: p.get("amount", 0),
"set_opening_inventory": lambda p: p.get("quantity", 0) * p.get("cost_per_unit", 0),
}


Expand Down Expand Up @@ -54,13 +60,15 @@ def _build_tool_map(self) -> dict:
from app.ai.tools.finance_tools import FinanceTools
from app.ai.tools.reporting_tools import ReportingTools
from app.ai.tools.action_tools import ActionTools
from app.ai.tools.extended_tools import ExtendedTools
from app.ai.rag.retriever import ERPContextRetriever

sales = SalesTools(self.db)
stock = StockTools(self.db)
finance = FinanceTools(self.db)
reporting = ReportingTools(self.db)
actions = ActionTools(self.db)
extended = ExtendedTools(self.db)
retriever = ERPContextRetriever(self.db)

return {
Expand Down Expand Up @@ -147,6 +155,121 @@ def _build_tool_map(self) -> dict:
payment_terms=p.get("payment_terms"),
notes=p.get("notes"),
),
# --- Extended: Opening Balances ---
"set_customer_opening_balance": lambda **p: extended.set_customer_opening_balance(
customer_id=p["customer_id"],
amount=p["amount"],
balance_type=p.get("balance_type", "debit"),
notes=p.get("notes"),
),
"set_supplier_opening_balance": lambda **p: extended.set_supplier_opening_balance(
supplier_id=p["supplier_id"],
amount=p["amount"],
balance_type=p.get("balance_type", "credit"),
notes=p.get("notes"),
),
"set_cash_opening_balance": lambda **p: extended.set_cash_opening_balance(
amount=p["amount"],
account_name=p.get("account_name", "الصندوق الرئيسي"),
notes=p.get("notes"),
),
"set_opening_inventory": lambda **p: extended.set_opening_inventory(
product_id=p["product_id"],
warehouse_id=p["warehouse_id"],
quantity=p["quantity"],
cost_per_unit=p["cost_per_unit"],
notes=p.get("notes"),
),
"get_opening_balances": lambda **p: extended.get_opening_balances(
entity_type=p.get("entity_type"),
),
# --- Extended: Expenses ---
"create_expense": lambda **p: extended.create_expense(
name=p["name"],
amount=p["amount"],
category=p.get("category", "Miscellaneous"),
notes=p.get("notes"),
expense_date=p.get("expense_date"),
),
"list_expenses": lambda **p: extended.list_expenses(
date_from=p.get("date_from"),
date_to=p.get("date_to"),
category=p.get("category"),
search=p.get("search"),
limit=p.get("limit", 20),
),
"get_expense_summary": lambda **_: extended.get_expense_summary(),
# --- Extended: Sales Invoice Retrieval ---
"list_sales_invoices": lambda **p: extended.list_sales_invoices(
limit=p.get("limit", 20),
status=p.get("status"),
),
"get_sales_invoice": lambda **p: extended.get_sales_invoice(p["invoice_id"]),
"get_invoice_items": lambda **p: extended.get_invoice_items(p["invoice_id"]),
"create_sales_return": lambda **p: extended.create_sales_return(
invoice_id=p["invoice_id"],
items=p["items"],
reason=p.get("reason"),
),
# --- Extended: Purchase Invoices ---
"list_purchase_invoices": lambda **p: extended.list_purchase_invoices(
limit=p.get("limit", 20),
),
"get_purchase_invoice": lambda **p: extended.get_purchase_invoice(p["purchase_invoice_id"]),
"get_purchase_items": lambda **p: extended.get_purchase_items(p["purchase_invoice_id"]),
"create_purchase_invoice": lambda **p: extended.create_purchase_invoice(
supplier_id=p["supplier_id"],
items=p["items"],
payment_type=p.get("payment_type", "cash"),
paid_amount=p.get("paid_amount"),
warehouse_id=p.get("warehouse_id", 1),
notes=p.get("notes"),
),
"create_purchase_return": lambda **p: extended.create_purchase_return(
purchase_invoice_id=p["purchase_invoice_id"],
items=p["items"],
reason=p.get("reason"),
),
# --- Extended: Suppliers ---
"create_supplier": lambda **p: extended.create_supplier(
name=p["name"],
phone=p.get("phone"),
address=p.get("address"),
notes=p.get("notes"),
),
"update_supplier": lambda **p: extended.update_supplier(
supplier_id=p["supplier_id"],
name=p.get("name"),
phone=p.get("phone"),
address=p.get("address"),
notes=p.get("notes"),
),
"search_suppliers": lambda **p: extended.search_suppliers(
query=p["query"],
limit=p.get("limit", 10),
),
# --- Extended: Products ---
"create_product": lambda **p: extended.create_product(
name=p["name"],
sku=p.get("sku"),
category_id=p.get("category_id"),
selling_price=p.get("selling_price", 0),
cost_price=p.get("cost_price", 0),
base_unit=p.get("base_unit", "meter"),
barcode=p.get("barcode"),
notes=p.get("notes"),
),
"update_product": lambda **p: extended.update_product(
product_id=p["product_id"],
name=p.get("name"),
selling_price=p.get("selling_price"),
cost_price=p.get("cost_price"),
category_id=p.get("category_id"),
base_unit=p.get("base_unit"),
barcode=p.get("barcode"),
notes=p.get("notes"),
),
"get_product": lambda **p: extended.get_product(p["product_id"]),
# --- Safety: Confirmation ---
"confirm_transaction": lambda **p: self._confirm_transaction(p["confirmation_id"]),
}
Expand Down Expand Up @@ -318,5 +441,28 @@ def _store_in_memory(self, tool_name: str, params: dict, result: dict):
name=name,
fact=f"عميل جديد. تليفون: {phone}. عنوان: {address}",
)
elif tool_name == "create_purchase_invoice":
self.vector_memory.store_transaction_fact(
customer_id=params.get("supplier_id", 0),
customer_name=f"مورد #{params.get('supplier_id', 0)}",
action="فاتورة مشتريات",
details={
"purchase_invoice_id": result.get("purchase_invoice_id"),
"total": result.get("total_amount", 0),
"items_count": len(params.get("items", [])),
},
)
elif tool_name == "create_expense":
self.vector_memory.store_transaction_fact(
customer_id=0,
customer_name="مصروفات",
action="مصروف جديد",
details={
"expense_id": result.get("expense_id"),
"name": params.get("name"),
"amount": params.get("amount", 0),
"category": params.get("category"),
},
)
except Exception as e:
logger.warning(f"Memory store failed (non-critical): {e}")
49 changes: 48 additions & 1 deletion backend/app/ai/safety/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,24 @@ def __init__(self, tool_name: str, role: str, reason: str):
"get_profit_and_loss", "get_cash_balance", "get_receivables_summary",
"get_payables_summary", "get_expense_breakdown", "get_daily_revenue",
"demand_forecast", "search_products", "search_customers",
# Extended read tools
"get_opening_balances", "list_expenses", "get_expense_summary",
"list_sales_invoices", "get_sales_invoice", "get_invoice_items",
"list_purchase_invoices", "get_purchase_invoice", "get_purchase_items",
"search_suppliers", "get_product",
# Write tools
"create_invoice", "cancel_invoice", "apply_discount",
"record_payment", "refund_payment",
"update_stock", "transfer_stock", "adjust_stock",
"create_customer", "update_customer",
# Extended write tools
"set_customer_opening_balance", "set_supplier_opening_balance",
"set_cash_opening_balance", "set_opening_inventory",
"create_expense",
"create_sales_return",
"create_purchase_invoice", "create_purchase_return",
"create_supplier", "update_supplier",
"create_product", "update_product",
"confirm_transaction",
],
},
Expand All @@ -58,31 +71,48 @@ def __init__(self, tool_name: str, role: str, reason: str):
"get_today_sales", "get_customer_info", "get_customer_history",
"get_unpaid_invoices", "get_stock_level",
"search_products", "search_customers",
# Extended read
"list_sales_invoices", "get_sales_invoice", "get_invoice_items",
"get_expense_summary", "get_product",
# Write
"create_invoice", "apply_discount",
"record_payment",
"create_customer", "update_customer",
"create_expense",
"confirm_transaction",
],
"blocked": [
"cancel_invoice", "refund_payment", "adjust_stock",
"transfer_stock", "update_stock",
"set_customer_opening_balance", "set_supplier_opening_balance",
"set_cash_opening_balance", "set_opening_inventory",
"create_purchase_invoice", "create_purchase_return",
"create_supplier", "update_supplier",
"create_product", "update_product",
],
},
"warehouse_employee": {
"allowed": [
# Read
"get_stock_level", "get_low_stock_items", "get_stock_movement_history",
"get_warehouse_summary", "get_dead_stock", "get_stock_valuation",
"search_products",
"search_products", "get_product",
# Extended read
"list_purchase_invoices", "get_purchase_invoice", "get_purchase_items",
"search_suppliers",
# Write
"update_stock", "transfer_stock",
"set_opening_inventory",
"confirm_transaction",
],
"blocked": [
"create_invoice", "cancel_invoice", "record_payment",
"refund_payment", "adjust_stock",
"create_customer", "update_customer",
"set_customer_opening_balance", "set_supplier_opening_balance",
"set_cash_opening_balance",
"create_expense",
"create_purchase_invoice", "create_purchase_return",
],
},
"accountant": {
Expand All @@ -94,14 +124,26 @@ def __init__(self, tool_name: str, role: str, reason: str):
"get_profit_and_loss", "get_cash_balance", "get_receivables_summary",
"get_payables_summary", "get_expense_breakdown", "get_daily_revenue",
"demand_forecast", "search_products", "search_customers",
# Extended read
"get_opening_balances", "list_expenses", "get_expense_summary",
"list_sales_invoices", "get_sales_invoice", "get_invoice_items",
"list_purchase_invoices", "get_purchase_invoice", "get_purchase_items",
"search_suppliers", "get_product",
# Limited write
"record_payment",
"set_customer_opening_balance", "set_supplier_opening_balance",
"set_cash_opening_balance", "set_opening_inventory",
"create_expense",
"confirm_transaction",
],
"blocked": [
"create_invoice", "cancel_invoice", "refund_payment",
"update_stock", "transfer_stock", "adjust_stock",
"create_customer",
"create_purchase_invoice", "create_purchase_return",
"create_sales_return",
"create_supplier", "update_supplier",
"create_product", "update_product",
],
},
# Default for AI when no user context is available
Expand All @@ -112,6 +154,11 @@ def __init__(self, tool_name: str, role: str, reason: str):
"get_stock_level", "get_low_stock_items",
"get_cash_balance", "get_receivables_summary",
"search_products", "search_customers",
# Extended read (safe)
"get_opening_balances", "list_expenses", "get_expense_summary",
"list_sales_invoices", "get_sales_invoice", "get_invoice_items",
"list_purchase_invoices", "get_purchase_invoice", "get_purchase_items",
"search_suppliers", "get_product",
],
"blocked": "*_write",
},
Expand Down
59 changes: 59 additions & 0 deletions backend/app/ai/safety/transaction_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
"adjust_stock",
"transfer_stock",
"update_stock",
# Extended write operations
"set_customer_opening_balance",
"set_supplier_opening_balance",
"set_cash_opening_balance",
"set_opening_inventory",
"create_expense",
"create_sales_return",
"create_purchase_invoice",
"create_purchase_return",
"create_supplier",
"create_product",
}

# Thresholds that trigger mandatory confirmation
Expand All @@ -33,6 +44,14 @@
"refund_payment": lambda params: params.get("amount", 0) > 1000,
"transfer_stock": lambda params: params.get("quantity", 0) > 100,
"adjust_stock": lambda params: True, # always confirm stock adjustments
"set_customer_opening_balance": lambda params: params.get("amount", 0) > 10000,
"set_supplier_opening_balance": lambda params: params.get("amount", 0) > 10000,
"set_cash_opening_balance": lambda params: params.get("amount", 0) > 50000,
"set_opening_inventory": lambda params: params.get("quantity", 0) * params.get("cost_per_unit", 0) > 20000,
"create_expense": lambda params: params.get("amount", 0) > 5000,
"create_purchase_invoice": lambda params: sum(i.get("quantity", 0) * i.get("purchase_price", 0) for i in params.get("items", [])) > 10000,
"create_sales_return": lambda params: True, # always confirm returns
"create_purchase_return": lambda params: True, # always confirm returns
}

PENDING_KEY_PREFIX = "ai:pending_tx:"
Expand Down Expand Up @@ -90,6 +109,24 @@ def dry_run(self, tool_name: str, params: dict) -> dict:
elif tool_name in ("transfer_stock", "update_stock", "adjust_stock"):
preview["quantity"] = params.get("quantity", params.get("new_quantity", 0))

elif tool_name == "create_expense":
preview["amount"] = params.get("amount", 0)
preview["category"] = params.get("category", "Miscellaneous")

elif tool_name == "create_purchase_invoice":
preview["item_count"] = len(params.get("items", []))
preview["estimated_total"] = sum(
i.get("quantity", 0) * i.get("purchase_price", 0)
for i in params.get("items", [])
)

elif tool_name in ("set_customer_opening_balance", "set_supplier_opening_balance", "set_cash_opening_balance"):
preview["amount"] = params.get("amount", 0)

elif tool_name == "set_opening_inventory":
preview["quantity"] = params.get("quantity", 0)
preview["total_value"] = params.get("quantity", 0) * params.get("cost_per_unit", 0)

return preview

def store_pending(self, session_id: str, tool_name: str, params: dict) -> str:
Expand Down Expand Up @@ -170,6 +207,19 @@ def _describe_operation(self, tool_name: str, params: dict) -> str:
"adjust_stock": lambda p: f"تعديل مخزون منتج {p.get('product_id')} إلى {p.get('new_quantity')} وحدة",
"create_customer": lambda p: f"إنشاء عميل جديد: {p.get('name')}",
"update_customer": lambda p: f"تعديل بيانات العميل {p.get('customer_id')}",
# Extended operations
"set_customer_opening_balance": lambda p: f"تسجيل رصيد أول المدة {p.get('amount')} جنيه للعميل #{p.get('customer_id')}",
"set_supplier_opening_balance": lambda p: f"تسجيل رصيد أول المدة {p.get('amount')} جنيه للمورد #{p.get('supplier_id')}",
"set_cash_opening_balance": lambda p: f"تسجيل رصيد الصندوق أول المدة {p.get('amount')} جنيه",
"set_opening_inventory": lambda p: f"تسجيل مخزون أول المدة: {p.get('quantity')} وحدة من المنتج #{p.get('product_id')}",
"create_expense": lambda p: f"تسجيل مصروف '{p.get('name')}' بمبلغ {p.get('amount')} جنيه",
"create_sales_return": lambda p: f"مرتجع مبيعات من الفاتورة #{p.get('invoice_id')} ({len(p.get('items', []))} أصناف)",
"create_purchase_invoice": lambda p: f"فاتورة مشتريات بـ {len(p.get('items', []))} أصناف للمورد #{p.get('supplier_id')}",
"create_purchase_return": lambda p: f"مرتجع مشتريات من الفاتورة #{p.get('purchase_invoice_id')} ({len(p.get('items', []))} أصناف)",
"create_supplier": lambda p: f"إنشاء مورد جديد: {p.get('name')}",
"update_supplier": lambda p: f"تعديل بيانات المورد #{p.get('supplier_id')}",
"create_product": lambda p: f"إنشاء منتج جديد: {p.get('name')}",
"update_product": lambda p: f"تعديل بيانات المنتج #{p.get('product_id')}",
}
fn = descriptions.get(tool_name)
return fn(params) if fn else f"تنفيذ {tool_name}"
Expand Down Expand Up @@ -197,4 +247,13 @@ def _get_reverse_action(self, tool_name: str, params: dict, result: dict) -> Opt
"notes": "rollback",
}}

elif tool_name == "create_purchase_invoice":
purchase_invoice_id = result.get("purchase_invoice_id")
if purchase_invoice_id:
return {"tool": "create_purchase_return", "params": {
"purchase_invoice_id": purchase_invoice_id,
"items": params.get("items", []),
"reason": "rollback",
}}

return None
Loading