ApiLogicServer 14.3.0__py3-none-any.whl → 14.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/METADATA +2 -2
  2. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/RECORD +46 -43
  3. api_logic_server_cli/api_logic_server.py +2 -1
  4. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  5. api_logic_server_cli/cli.py +5 -2
  6. api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
  7. api_logic_server_cli/create_from_model/ont_build.py +1 -1
  8. api_logic_server_cli/genai/genai.py +4 -0
  9. api_logic_server_cli/genai/genai_svcs.py +2 -0
  10. api_logic_server_cli/manager.py +20 -16
  11. api_logic_server_cli/prototypes/base/api/system/expression_parser.py +10 -4
  12. api_logic_server_cli/prototypes/base/integration/kafka/kafka_producer.py +32 -8
  13. api_logic_server_cli/prototypes/base/integration/system/RowDictMapper.py +33 -16
  14. api_logic_server_cli/prototypes/base/logic/declare_logic.py +1 -0
  15. api_logic_server_cli/prototypes/base/logic/load_verify_rules.py +2 -1
  16. api_logic_server_cli/prototypes/genai_demo/api/customize_api.py +9 -11
  17. api_logic_server_cli/prototypes/genai_demo/database/.DS_Store +0 -0
  18. api_logic_server_cli/prototypes/genai_demo/database/db.sqlite +0 -0
  19. api_logic_server_cli/prototypes/genai_demo/database/models.py +52 -42
  20. api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/OrderB2B.py +4 -6
  21. api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/__pycache__/OrderB2B.cpython-312.pyc +0 -0
  22. api_logic_server_cli/prototypes/genai_demo/integration/row_dict_maps/row_dict_maps_readme.md +3 -0
  23. api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/declare_logic.cpython-312.pyc +0 -0
  24. api_logic_server_cli/prototypes/genai_demo/logic/__pycache__/load_verify_rules.cpython-312.pyc +0 -0
  25. api_logic_server_cli/prototypes/genai_demo/logic/declare_logic.py +57 -69
  26. api_logic_server_cli/prototypes/genai_demo/logic/load_verify_rules.py +216 -0
  27. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/__init__.cpython-312.pyc +0 -0
  28. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/auto_discovery.cpython-312.pyc +0 -0
  29. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/__pycache__/error_testing.cpython-312.pyc +0 -0
  30. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/auto_discovery.py +52 -0
  31. api_logic_server_cli/prototypes/genai_demo/logic/readme_declare_logic.md +172 -0
  32. api_logic_server_cli/prototypes/genai_demo/security/__pycache__/declare_security.cpython-312.pyc +0 -0
  33. api_logic_server_cli/prototypes/genai_demo/ui/admin/admin.yaml +86 -53
  34. api_logic_server_cli/prototypes/manager/.vscode/launch.json +1 -1
  35. api_logic_server_cli/prototypes/manager/README.md +1 -1
  36. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.prompt +4 -1
  37. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo.response_example +15 -8
  38. api_logic_server_cli/prototypes/manager/system/genai/examples/genai_demo/genai_demo_informal.prompt +3 -0
  39. api_logic_server_cli/prototypes/manager/system/genai/examples/time_tracking_billing/readme.md +58 -5
  40. api_logic_server_cli/prototypes/manager/system/genai/learning_requests/logic_bank_api.prompt +15 -1
  41. api_logic_server_cli/sqlacodegen_wrapper/sqlacodegen/sqlacodegen/__pycache__/codegen.cpython-312.pyc +0 -0
  42. api_logic_server_cli/sqlacodegen_wrapper/sqlacodegen/sqlacodegen/codegen.py +2 -1
  43. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/__pycache__/copilot_models.cpython-312.pyc +0 -0
  44. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/__pycache__/sample_ai_models.cpython-312.pyc +0 -0
  45. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.chatgpt +0 -16
  46. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.sql +0 -66
  47. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai.sqlite +0 -0
  48. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_items.sqlite +0 -0
  49. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_models.py +0 -156
  50. api_logic_server_cli/prototypes/genai_demo/database/chatgpt/sample_ai_models.sqlite +0 -0
  51. api_logic_server_cli/prototypes/genai_demo/logic/cocktail-napkin.jpg +0 -0
  52. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/LICENSE +0 -0
  53. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/WHEEL +0 -0
  54. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/entry_points.txt +0 -0
  55. {ApiLogicServer-14.3.0.dist-info → ApiLogicServer-14.3.7.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  # coding: utf-8
2
- from sqlalchemy import Boolean, Column, DECIMAL, DateTime, ForeignKey, Integer, Text, text
2
+ from sqlalchemy import DECIMAL, DateTime # API Logic Server GenAI assist
3
+ from sqlalchemy import Column, Date, ForeignKey, Integer, Numeric, String, Boolean
3
4
  from sqlalchemy.orm import relationship
4
5
  from sqlalchemy.ext.declarative import declarative_base
5
6
 
@@ -9,13 +10,13 @@ from sqlalchemy.ext.declarative import declarative_base
9
10
  # Alter this file per your database maintenance policy
10
11
  # See https://apilogicserver.github.io/Docs/Project-Rebuild/#rebuilding
11
12
  #
12
- # Created: May 03, 2024 22:23:08
13
- # Database: sqlite:////Users/val/dev/ApiLogicServer/ApiLogicServer-dev/clean/ApiLogicServer/genai_demo/database/db.sqlite
13
+ # Created: January 31, 2025 17:52:29
14
+ # Database: sqlite:////Users/val/dev/ApiLogicServer/ApiLogicServer-dev/build_and_test/ApiLogicServer/genai_demo/database/db.sqlite
14
15
  # Dialect: sqlite
15
16
  #
16
17
  # mypy: ignore-errors
17
18
  ########################################################################################################################
18
-
19
+
19
20
  from database.system.SAFRSBaseX import SAFRSBaseX, TestBase
20
21
  from flask_login import UserMixin
21
22
  import safrs, flask_sqlalchemy, os
@@ -42,72 +43,81 @@ else:
42
43
  print('*** Models.py Using TestBase ***')
43
44
 
44
45
 
45
- class Customer(Base):
46
- __tablename__ = 'Customers'
46
+
47
+ class Customer(Base): # type: ignore
48
+ """
49
+ description: Customer table with unique name, balance and credit_limit.
50
+ """
51
+ __tablename__ = 'customer'
47
52
  _s_collection_name = 'Customer' # type: ignore
48
53
 
49
- CustomerID = Column(Integer, primary_key=True)
50
- CustomerName = Column(Text, nullable=False)
51
- Address = Column(Text)
52
- Phone = Column(Text)
53
- Balance : DECIMAL = Column(DECIMAL(10, 2))
54
- CreditLimit : DECIMAL = Column(DECIMAL(10, 2), nullable=False)
54
+ id = Column(Integer, primary_key=True)
55
+ name = Column(String(255), unique=True)
56
+ balance = Column(Numeric)
57
+ credit_limit = Column(Numeric)
55
58
 
56
59
  # parent relationships (access parent)
57
60
 
58
61
  # child relationships (access children)
59
- OrderList : Mapped[List["Order"]] = relationship(back_populates="Customer")
62
+ OrderList : Mapped[List["Order"]] = relationship(back_populates="customer")
60
63
 
61
64
 
62
- class Product(Base):
63
- __tablename__ = 'Products'
64
- _s_collection_name = 'Product' # type: ignore
65
65
 
66
- ProductID = Column(Integer, primary_key=True)
67
- ProductName = Column(Text, nullable=False)
68
- UnitPrice : DECIMAL = Column(DECIMAL(10, 2), nullable=False)
69
- CarbonNeutral = Column(Boolean)
66
+ class Product(Base): # type: ignore
67
+ """
68
+ description: Product table with unit_price used for copying to Item.
69
+ """
70
+ __tablename__ = 'product'
71
+ _s_collection_name = 'Product' # type: ignore
70
72
 
73
+ id = Column(Integer, primary_key=True)
74
+ name = Column(String(255))
75
+ unit_price = Column(Numeric)
76
+ carbon_neutral = Column(Boolean)
71
77
  # parent relationships (access parent)
72
78
 
73
79
  # child relationships (access children)
74
- ItemList : Mapped[List["Item"]] = relationship(back_populates="Product")
80
+ ItemList : Mapped[List["Item"]] = relationship(back_populates="product")
75
81
 
76
82
 
77
83
 
78
- class Order(Base):
79
- __tablename__ = 'Orders'
84
+ class Order(Base): # type: ignore
85
+ """
86
+ description: Order table with a foreign key to Customer, a notes field, date_shipped and derived amount_total.
87
+ """
88
+ __tablename__ = 'order'
80
89
  _s_collection_name = 'Order' # type: ignore
81
90
 
82
- OrderID = Column(Integer, primary_key=True)
83
- CustomerID = Column(ForeignKey('Customers.CustomerID'))
84
- OrderDate = Column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
85
- Notes = Column(Text)
86
- ShipDate = Column(DateTime)
87
- AmountTotal : DECIMAL = Column(DECIMAL(10, 2))
91
+ id = Column(Integer, primary_key=True)
92
+ customer_id = Column(ForeignKey('customer.id'))
93
+ date_shipped = Column(Date)
94
+ notes = Column(String(255))
95
+ amount_total = Column(Numeric)
88
96
 
89
97
  # parent relationships (access parent)
90
- Customer : Mapped["Customer"] = relationship(back_populates=("OrderList"))
98
+ customer : Mapped["Customer"] = relationship(back_populates=("OrderList"))
91
99
 
92
100
  # child relationships (access children)
93
- ItemList : Mapped[List["Item"]] = relationship(back_populates="Order")
101
+ ItemList : Mapped[List["Item"]] = relationship(back_populates="order")
94
102
 
95
103
 
96
104
 
97
- class Item(Base):
98
- __tablename__ = 'Items'
105
+ class Item(Base): # type: ignore
106
+ """
107
+ description: Item table with non-null quantity, derived amount and unit_price copied from Product.
108
+ """
109
+ __tablename__ = 'item'
99
110
  _s_collection_name = 'Item' # type: ignore
100
111
 
101
- ItemID = Column(Integer, primary_key=True)
102
- OrderID = Column(ForeignKey('Orders.OrderID'))
103
- ProductID = Column(ForeignKey('Products.ProductID'))
104
- Quantity = Column(Integer, nullable=False)
105
- UnitPrice : DECIMAL = Column(DECIMAL(10, 2))
106
- Amount : DECIMAL = Column(DECIMAL(10, 2))
112
+ id = Column(Integer, primary_key=True)
113
+ order_id = Column(ForeignKey('order.id'))
114
+ product_id = Column(ForeignKey('product.id'))
115
+ quantity = Column(Integer, nullable=False)
116
+ unit_price = Column(Numeric)
117
+ amount = Column(Numeric)
107
118
 
108
119
  # parent relationships (access parent)
109
- Order : Mapped["Order"] = relationship(back_populates=("ItemList"))
110
- Product : Mapped["Product"] = relationship(back_populates=("ItemList"))
120
+ order : Mapped["Order"] = relationship(back_populates=("ItemList"))
121
+ product : Mapped["Product"] = relationship(back_populates=("ItemList"))
111
122
 
112
123
  # child relationships (access children)
113
-
@@ -19,15 +19,13 @@ class OrderB2B(RowDictMapper):
19
19
  order = super(OrderB2B, self).__init__(
20
20
  model_class=models.Order
21
21
  , alias = "order"
22
- , fields = [(models.Order.Notes)]
23
- , parent_lookups = [( models.Customer,
24
- [(models.Customer.CustomerName, 'Account')]
25
- )]
22
+ , fields = [(models.Order.notes, "Notes")]
23
+ , parent_lookups = [( models.Customer, [(models.Customer.name, 'Account')] )]
26
24
  , related = [
27
25
  (RowDictMapper(model_class=models.Item
28
26
  , alias="Items"
29
- , fields = [(models.Item.Quantity, "QuantityOrdered")]
30
- , parent_lookups = [( models.Product, [models.Product.ProductName] )]
27
+ , fields = [(models.Item.quantity, "QuantityOrdered")]
28
+ , parent_lookups = [( models.Product, [(models.Product.name, 'ProductName')] )]
31
29
  )
32
30
  )
33
31
  ]
@@ -0,0 +1,3 @@
1
+ Create definitions here to configure mappings for row <-> dicts, typically for application integration.
2
+
3
+ To see a Sample Integration, [click here](https://apilogicserver.github.io/Docs/Sample-Integration/).
@@ -1,16 +1,15 @@
1
- import datetime
1
+ import datetime, os
2
2
  from decimal import Decimal
3
3
  from logic_bank.exec_row_logic.logic_row import LogicRow
4
4
  from logic_bank.extensions.rule_extensions import RuleExtension
5
5
  from logic_bank.logic_bank import Rule
6
- from database.models import *
7
- from database.models import Customer, Order, Item, Product
6
+ from logic_bank.logic_bank import DeclareRule
7
+ import database.models as models
8
8
  import api.system.opt_locking.opt_locking as opt_locking
9
- from security.system.authorization import Grant
10
- import logging, os
11
- from integration.row_dict_maps.OrderShipping import OrderShipping
12
- from confluent_kafka import Producer, KafkaException
9
+ from security.system.authorization import Grant, Security
10
+ from logic.load_verify_rules import load_verify_rules
13
11
  import integration.kafka.kafka_producer as kafka_producer
12
+ import logging
14
13
 
15
14
  app_logger = logging.getLogger(__name__)
16
15
 
@@ -20,104 +19,93 @@ def declare_logic():
20
19
  ''' Declarative multi-table derivations and constraints, extensible with Python.
21
20
 
22
21
  Brief background: see readme_declare_logic.md
23
-
24
- GenAI: the following prompt was sent to GenAI-Logic, which translated it into the code below:
25
-
26
- Use case: Check Credit
27
22
 
28
- 1. The Customer's balance is less than the credit limit
29
- 2. The Customer's balance is the sum of the Order amount_total where date_shipped is null
30
- 3. The Order's amount_total is the sum of the Item amount
31
- 4. The Item amount is the quantity * unit_price
32
- 5. The Item unit_price is copied from the Product unit_price
23
+ Your Code Goes Here - Use code completion (Rule.) to declare rules
33
24
  '''
34
25
 
35
- from logic_bank.logic_bank import Rule
36
-
37
- from logic.logic_discovery.auto_discovery import discover_logic
38
- discover_logic()
26
+ if os.environ.get("WG_PROJECT"):
27
+ # Inside WG: Load rules from docs/expprt/export.json
28
+ load_verify_rules()
29
+ else:
30
+ # Outside WG: load declare_logic function
31
+ from logic.logic_discovery.auto_discovery import discover_logic
32
+ discover_logic()
39
33
 
40
34
  # Logic from GenAI: (or, use your IDE w/ code completion)
35
+ from database.models import Product, Customer, Item, Order
41
36
 
42
- # Ensures the customer's balance is less than the credit limit.
37
+ # Ensure that Customer balance does not exceed credit_limit.
43
38
  Rule.constraint(validate=Customer,
44
- as_condition=lambda row: row.Balance <= row.CreditLimit,
45
- error_msg="Customer balance ({row.Balance}) exceeds credit limit ({row.CreditLimit})")
46
-
47
- # Computes the customer's balance as the sum of unshipped orders.
48
- Rule.sum(derive=Customer.Balance, as_sum_of=Order.AmountTotal, where=lambda row: row.ShipDate is None)
49
-
50
- # Computes the total amount of an order as the sum of item amounts.
51
- Rule.sum(derive=Order.AmountTotal, as_sum_of=Item.Amount)
52
-
53
- # Calculates the item amount as the quantity times unit price. (original rule, prior to iteration)
54
- # Rule.formula(derive=Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
55
-
56
- # Copies the unit price from the parent product to the item.
57
- Rule.copy(derive=Item.UnitPrice, from_parent=Product.UnitPrice)
58
-
59
- # End Logic from GenAI
39
+ as_condition=lambda row: row.balance <= row.credit_limit,
40
+ error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
60
41
 
42
+ # Customer.balance is the sum of Order.amount_total for orders not yet shipped.
43
+ Rule.sum(derive=Customer.balance, as_sum_of=Order.amount_total, where=lambda row: row.date_shipped is None)
61
44
 
62
- #als: Demonstrate that logic == Rules + Python (for extensibility)
45
+ # Order.amount_total is the sum of Item.amount.
46
+ Rule.sum(derive=Order.amount_total, as_sum_of=Item.amount)
63
47
 
64
- # 4. Items.Amount = Quantity * UnitPrice, altered with IDE for CarbonNeutral discount
65
- def derive_amount(row: Item, old_row: Item, logic_row: LogicRow):
48
+ def derive_amount(row: models.Item, old_row: models.Item, logic_row: LogicRow):
66
49
  amount = row.Quantity * row.UnitPrice
67
50
  if row.Product.CarbonNeutral == True and row.Quantity >= 10:
68
51
  amount = amount * Decimal(0.9) # breakpoint here
69
52
  return amount
70
- # now register function; note logic ordering is automatic
71
- Rule.formula(derive=Item.Amount, calling=derive_amount)
72
53
 
73
- def send_order_to_shipping(row: Order, old_row: Order, logic_row: LogicRow):
74
- """ #als: Send Kafka message formatted by OrderShipping RowDictMapper
54
+ # Items.Amount = Quantity * UnitPrice with discount for CarbonNeutral products.
55
+ Rule.formula(derive=models.Item.Amount,
56
+ calling=derive_amount)
75
57
 
76
- Format row per shipping requirements, and send (e.g., a message)
58
+ # Item.unit_price is copied from Product.unit_price.
59
+ Rule.copy(derive=Item.unit_price, from_parent=Product.unit_price)
77
60
 
78
- NB: the after_flush event makes Order.Id avaible.
79
-
80
- Args:
81
- row (Order): inserted Order
82
- old_row (Order): n/a
83
- logic_row (LogicRow): bundles curr/old row, with ins/upd/dlt (etc) state
84
- """
85
- if logic_row.is_inserted():
86
- kafka_producer.send_kafka_message(logic_row=logic_row,
87
- row_dict_mapper=OrderShipping,
88
- kafka_topic="order_shipping",
89
- kafka_key=str(row.OrderID),
90
- msg="Sending Order to Shipping")
91
-
92
- Rule.after_flush_row_event(on_class=Order, calling=send_order_to_shipping) # see above
61
+ # Send the Order to Kafka topic 'order_shipping' if the date_shipped is not None.
62
+ Rule.after_flush_row_event(on_class=Order, calling=kafka_producer.send_row_to_kafka,
63
+ if_condition=lambda row: row.date_shipped is not None,
64
+ with_args={'topic': 'order_shipping'})
93
65
 
66
+ # End Logic from GenAI
94
67
 
95
68
 
96
- def handle_all(logic_row: LogicRow): # OPTIMISTIC LOCKING, [TIME / DATE STAMPING]
69
+ def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
97
70
  """
98
71
  This is generic - executed for all classes.
99
72
 
100
- Invokes optimistic locking.
73
+ Invokes optimistic locking, and checks Grant permissions.
101
74
 
102
- You can optionally do time and date stamping here, as shown below.
75
+ Also provides user/date stamping.
103
76
 
104
77
  Args:
105
78
  logic_row (LogicRow): from LogicBank - old/new row, state
106
79
  """
107
80
 
108
- if not os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
81
+ if os.getenv("APILOGICPROJECT_NO_FLASK") is not None:
82
+ print("\ndeclare_logic.py Using TestBase\n")
109
83
  return # enables rules to be used outside of Flask, e.g., test data loading
110
84
 
111
85
  if logic_row.is_updated() and logic_row.old_row is not None and logic_row.nest_level == 0:
112
86
  opt_locking.opt_lock_patch(logic_row=logic_row)
113
- enable_creation_stamping = False # CreatedOn time stamping
114
- if enable_creation_stamping:
87
+
88
+ Grant.process_updates(logic_row=logic_row)
89
+
90
+ did_stamping = False
91
+ if enable_stamping := False: # #als: DATE / USER STAMPING
115
92
  row = logic_row.row
116
93
  if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
117
94
  row.CreatedOn = datetime.datetime.now()
118
- logic_row.log("early_row_event_all_classes - handle_all sets 'Created_on"'')
119
- Grant.process_updates(logic_row=logic_row)
120
-
95
+ did_stamping = True
96
+ if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedBy"):
97
+ row.CreatedBy = Security.current_user().id
98
+ # if Config.SECURITY_ENABLED == True else 'public'
99
+ did_stamping = True
100
+ if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedOn"):
101
+ row.UpdatedOn = datetime.datetime.now()
102
+ did_stamping = True
103
+ if logic_row.ins_upd_dlt == "upd" and hasattr(row, "UpdatedBy"):
104
+ row.UpdatedBy = Security.current_user().id \
105
+ if Config.SECURITY_ENABLED == True else 'public'
106
+ did_stamping = True
107
+ if did_stamping:
108
+ logic_row.log("early_row_event_all_classes - handle_all did stamping")
121
109
  Rule.early_row_event_all_classes(early_row_event_all_classes=handle_all)
122
110
 
123
111
  #als rules report
@@ -0,0 +1,216 @@
1
+ #
2
+ # This code loads and verifies rules from export.json and activates them if they pass verification
3
+ # It is WebGenAI specific, used only when env var WG_PROJECT is set
4
+ #
5
+ import ast
6
+ import json
7
+ import logging
8
+ import os
9
+ import sys
10
+ import safrs
11
+ import subprocess
12
+ from importlib import import_module
13
+ from pathlib import Path
14
+ from werkzeug.utils import secure_filename
15
+ from database.models import *
16
+ from logic_bank.logic_bank import DeclareRule, Rule, LogicBank
17
+ from colorama import Fore, Style, init
18
+ from logic_bank.logic_bank import RuleBank
19
+ from logic_bank.rule_bank.rule_bank_setup import find_referenced_attributes
20
+ import tempfile
21
+
22
+
23
+ app_logger = logging.getLogger(__name__)
24
+ declare_logic_message = "ALERT: *** No Rules Yet ***" # printed in api_logic_server.py
25
+
26
+ rule_import_template = """
27
+ from logic_bank.logic_bank import Rule
28
+ from database.models import *
29
+
30
+ def init_rule():
31
+ {rule_code}
32
+ """
33
+
34
+ MANAGER_PATH = "/opt/webgenai/database/manager.py"
35
+ EXPORT_JSON_PATH = os.environ.get("EXPORT_JSON_PATH", "./docs/export/export.json")
36
+
37
+
38
+ def set_rule_status(rule_id, status):
39
+ """
40
+ Call the manager.py script to set the status of a rule
41
+
42
+ (if the status is "active", the manager will remove the rule error)
43
+ """
44
+ if not Path(MANAGER_PATH).exists():
45
+ app_logger.info(f"No manager, can't set rule {rule_id} status {status}")
46
+ return
47
+ subprocess.run([
48
+ "python", MANAGER_PATH,
49
+ "-R", rule_id,
50
+ "--rule-status", status],
51
+ cwd="/opt/webgenai")
52
+
53
+
54
+ def set_rule_error(rule_id, error):
55
+ """
56
+ Call the manager.py script to set the error of a rule
57
+ """
58
+ if not Path(MANAGER_PATH).exists():
59
+ app_logger.warning(f"No manager, can't set rule {rule_id} error {error}")
60
+ return
61
+ subprocess.check_output([
62
+ "python", MANAGER_PATH,
63
+ "-R", rule_id,
64
+ "--rule-error", error],
65
+ cwd="/opt/webgenai")
66
+
67
+
68
+ def check_rule_code_syntax(rule_code):
69
+ """
70
+ Check the syntax of the rule code
71
+ """
72
+ try:
73
+ ast.parse(rule_code)
74
+ return rule_code
75
+ except Exception as exc:
76
+ log.warning(f"Syntax error in rule code '{rule_code}': {exc}")
77
+
78
+ rule_code = rule_code.replace("\\\\", "\\")
79
+ try:
80
+ ast.parse(rule_code)
81
+ return rule_code
82
+ except Exception as exc:
83
+ log.warning(f"Syntax error in rule code '{rule_code}': {exc}")
84
+ return None
85
+
86
+
87
+ def get_exported_rules(rule_code_dir):
88
+ """
89
+ Read the exported rules from export.json and write the code to the
90
+ rule_code_dir
91
+ """
92
+ export_file = Path(EXPORT_JSON_PATH)
93
+ if not export_file.exists():
94
+ app_logger.info(f"{export_file.resolve()} does not exist")
95
+ return []
96
+
97
+ try:
98
+ with open(export_file) as f:
99
+ export = json.load(f)
100
+ rules = export.get("rules", [])
101
+ except Exception as exc:
102
+ app_logger.warning(f"Failed to load rules from {export_file}: {exc}")
103
+ return []
104
+
105
+ for rule in rules:
106
+ if rule["status"] == "rejected":
107
+ continue
108
+ rule_file = rule_code_dir / f"{secure_filename(rule['name']).replace('.','_')}.py"
109
+ try:
110
+ # write current rule to rule_file
111
+ # (we can't use eval, because logicbank uses inspect)
112
+ rule_code_str = check_rule_code_syntax(rule["code"])
113
+ if not rule_code_str:
114
+ continue
115
+ with open(rule_file, "w") as temp_file:
116
+ rule_code = "\n".join([f" {code}" for code in rule_code_str.split("\n")])
117
+ temp_file.write(rule_import_template.format(rule_code=rule_code))
118
+ temp_file_path = temp_file.name
119
+ # module_name used to import current rule
120
+ module_name = Path(temp_file_path).stem
121
+ rule["module_name"] = module_name
122
+ app_logger.info(f"{rule['id']} rule file: {rule_file}")
123
+ except Exception as exc:
124
+ app_logger.exception(exc)
125
+ app_logger.warning(f"Failed to write rule code to {rule_file}: {exc}")
126
+
127
+ return rules
128
+
129
+
130
+ def verify_rules(rule_code_dir, rule_type="accepted"):
131
+ """
132
+ Verify the rules from export.json and activate them if they pass verification
133
+
134
+ Write the rule code to a temporary file and import it as a module
135
+ """
136
+ rules = get_exported_rules(rule_code_dir)
137
+
138
+ for rule in rules:
139
+ if not rule["status"] == rule_type:
140
+ continue
141
+ module_name = rule["module_name"]
142
+ app_logger.info(f"\n{Fore.BLUE}Verifying rule: {module_name} - {rule['id']}{Style.RESET_ALL}")
143
+ try:
144
+ rule_module = import_module(module_name)
145
+ rule_module.init_rule()
146
+ LogicBank.activate(session=safrs.DB.session, activator=rule_module.init_rule)
147
+ if rule["status"] != "active":
148
+ set_rule_status(rule["id"], "active")
149
+ app_logger.info(f"\n{Fore.GREEN}Activated rule {rule['id']}{Style.RESET_ALL}")
150
+
151
+ except Exception as exc:
152
+ app_logger.exception(exc)
153
+ set_rule_error(rule["id"], f"{type(exc).__name__}: {exc}")
154
+ app_logger.warning(f"{Fore.RED}Failed to verify {rule_type} rule code\n{rule['code']}\n{Fore.YELLOW}{type(exc).__name__}: {exc}{Style.RESET_ALL}")
155
+ app_logger.debug(f"{rule}")
156
+ rule["status"] = "accepted"
157
+ rule["error"] = f"{type(exc).__name__}: {exc}"
158
+
159
+ return rules
160
+
161
+
162
+ def load_active_rules(rule_code_dir, rules=None):
163
+ """
164
+ Load the active rules from export.json
165
+ """
166
+ if not rules:
167
+ rules = get_exported_rules(rule_code_dir)
168
+ for rule in rules:
169
+ module_name = rule.get("module_name", None)
170
+ if not rule["status"] == "active" or module_name is None:
171
+ continue
172
+ app_logger.info(f"{Fore.GREEN}Loading Rule Module {module_name} {rule['id']} {Style.RESET_ALL}")
173
+ rule_module = import_module(module_name)
174
+ try:
175
+ rule_module.init_rule()
176
+ except Exception as exc:
177
+ app_logger.exception(exc)
178
+ app_logger.warning(f"{Fore.RED}Failed to load active rule {rule['id']} {rule['code']} {Style.RESET_ALL}")
179
+ set_rule_error(rule["id"], f"{type(exc).__name__}: {exc}")
180
+
181
+
182
+ def get_project_id():
183
+ if os.environ.get("PROJECT_ID"):
184
+ return os.environ.get("PROJECT_ID")
185
+
186
+ return Path(os.getcwd()).name
187
+
188
+
189
+ def load_verify_rules():
190
+
191
+ # Add FileHandler to root_logger
192
+ log_file = Path(os.getenv("LOG_DIR",tempfile.mkdtemp())) / "load_verify_rules.log"
193
+ file_handler = logging.FileHandler(log_file)
194
+ root_logger = logging.getLogger()
195
+ root_logger.addHandler(file_handler)
196
+
197
+ rule_code_dir = Path("./logic/wg_rules") # in the project root
198
+ rule_code_dir.mkdir(parents=True, exist_ok=True)
199
+ sys.path.append(f"{rule_code_dir}")
200
+
201
+ app_logger.info(f"Loading rules from {rule_code_dir.resolve()}")
202
+
203
+ rules = []
204
+
205
+ if os.environ.get("VERIFY_RULES") == "True":
206
+ rules = verify_rules(rule_code_dir, rule_type="active")
207
+ verify_rules(rule_code_dir, rule_type="accepted")
208
+ else:
209
+ try:
210
+ load_active_rules(rule_code_dir, rules)
211
+ except Exception as exc:
212
+ app_logger.exception(exc)
213
+ app_logger.warning(f"{Fore.RED}Failed to load active exported rules: {exc}{Style.RESET_ALL}")
214
+
215
+ root_logger.removeHandler(file_handler)
216
+
@@ -0,0 +1,52 @@
1
+ import importlib
2
+ from pathlib import Path
3
+ import logging
4
+
5
+ app_logger = logging.getLogger(__name__)
6
+
7
+ def discover_logic():
8
+ """
9
+ Discover additional logic in this directory
10
+ """
11
+ import os
12
+ logic = []
13
+ logic_path = Path(__file__).parent
14
+ for root, dirs, files in os.walk(logic_path):
15
+ for file in files:
16
+ if file.endswith(".py"):
17
+ spec = importlib.util.spec_from_file_location("module.name", logic_path.joinpath(file))
18
+ if file.endswith("auto_discovery.py"):
19
+ pass
20
+ else:
21
+ logic.append(file)
22
+ each_logic_file = importlib.util.module_from_spec(spec)
23
+ spec.loader.exec_module(each_logic_file) # runs "bare" module code (e.g., initialization)
24
+ each_logic_file.declare_logic() # invoke create function
25
+
26
+ # if False and Path(__file__).parent.parent.parent.joinpath("docs/project_is_genai_demo.txt").exists():
27
+ # return # for genai_demo, logic is in logic/declare_logic.py (so ignore logic_discovery)
28
+
29
+ wg_logic_path = Path(__file__).parent.parent.joinpath("wg_rules")
30
+ if wg_logic_path.exists():
31
+ run_local = os.environ.get("WG_PROJECT") is None # eg, running export locally
32
+ # run_local = False # for debug
33
+ if run_local:
34
+ wg_export_logic_path = Path(__file__).parent.parent.parent.joinpath("logic/wg_rules/active_rules_export.py")
35
+ if wg_export_logic_path.is_file():
36
+ spec = importlib.util.spec_from_file_location("module.name", wg_export_logic_path)
37
+ logic.append(str(wg_export_logic_path))
38
+ wg_export_logic_file = importlib.util.module_from_spec(spec)
39
+ spec.loader.exec_module(wg_export_logic_file) # runs "bare" module code (e.g., initialization)
40
+ wg_export_logic_file.declare_logic() # invoke create function
41
+ else:
42
+ for root, dirs, files in os.walk(wg_logic_path):
43
+ for file in files:
44
+ if file.endswith(".py") and 'active_rules_export.py' != file:
45
+ spec = importlib.util.spec_from_file_location("module.name", wg_logic_path.joinpath(file))
46
+ logic.append(file)
47
+ each_logic_file = importlib.util.module_from_spec(spec)
48
+ spec.loader.exec_module(each_logic_file) # runs "bare" module code (e.g., initialization)
49
+ each_logic_file.init_rule() # invoke create function
50
+
51
+ app_logger.info(f"..discovered logic: {logic}")
52
+ return