ApiLogicServer 15.4.3__py3-none-any.whl → 16.0.2__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.
- api_logic_server_cli/add_cust/add_cust.py +6 -2
- api_logic_server_cli/api_logic_server.py +2 -1
- api_logic_server_cli/database/basic_demo.sqlite +0 -0
- api_logic_server_cli/prototypes/base/.github/.copilot-instructions.md +229 -76
- api_logic_server_cli/prototypes/base/docs/training/OVERVIEW.md +64 -0
- api_logic_server_cli/prototypes/base/docs/training/README.md +140 -0
- api_logic_server_cli/prototypes/base/docs/training/genai_logic_patterns.md +443 -0
- api_logic_server_cli/prototypes/base/docs/training/logic_bank_api.prompt +23 -0
- api_logic_server_cli/prototypes/base/docs/training/logic_bank_patterns.prompt +445 -0
- api_logic_server_cli/prototypes/base/docs/training/probabilistic_logic.prompt +1081 -0
- api_logic_server_cli/prototypes/base/docs/training/probabilistic_logic_guide.md +483 -0
- api_logic_server_cli/prototypes/base/docs/training/probabilistic_template.py +326 -0
- api_logic_server_cli/prototypes/base/logic/logic_discovery/auto_discovery.py +8 -9
- api_logic_server_cli/prototypes/basic_demo/.github/.copilot-instructions.md +327 -141
- api_logic_server_cli/prototypes/basic_demo/.github/welcome.md +21 -7
- api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/manager/.github/.copilot-instructions.md +61 -155
- api_logic_server_cli/prototypes/manager/.github/welcome.md +43 -0
- api_logic_server_cli/prototypes/manager/.vscode/settings.json +1 -0
- api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/.github/.copilot-instructions.md +502 -76
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.2.dist-info}/METADATA +1 -1
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.2.dist-info}/RECORD +27 -19
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.2.dist-info}/WHEEL +0 -0
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.2.dist-info}/entry_points.txt +0 -0
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.2.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# Probabilistic Logic Guide - Pattern Implementation
|
|
2
|
+
|
|
3
|
+
**Scope:** Implementing probabilistic (AI-powered) logic rules in ApiLogicServer projects.
|
|
4
|
+
**Prerequisites:** Understanding of LogicBank deterministic rules (constraints, sums, formulas).
|
|
5
|
+
**Related:** See `genai_logic_patterns.md` for framework-level patterns.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Probabilistic logic extends deterministic rules with AI-powered decision making. The key pattern:
|
|
12
|
+
- **Deterministic rules:** Constraints, sums, copy - always produce same result
|
|
13
|
+
- **Probabilistic rules:** AI-powered formulas - results depend on data, context, external factors
|
|
14
|
+
|
|
15
|
+
**Architecture:**
|
|
16
|
+
```
|
|
17
|
+
logic/
|
|
18
|
+
declare_logic.py # Main rule declarations (loads auto-discovery)
|
|
19
|
+
logic_discovery/ # Use case logic
|
|
20
|
+
check_credit.py # Deterministic + conditional AI
|
|
21
|
+
ai_requests/ # Reusable AI handlers
|
|
22
|
+
supplier_selection.py # AI handler with Request Pattern
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Pattern: Conditional Formula with AI
|
|
28
|
+
|
|
29
|
+
### Basic Structure
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
def ConditionalFormula(row, old_row, logic_row):
|
|
33
|
+
"""
|
|
34
|
+
Conditional logic:
|
|
35
|
+
- IF condition met → use deterministic calculation
|
|
36
|
+
- ELSE → call AI for probabilistic computation
|
|
37
|
+
"""
|
|
38
|
+
if simple_case(row):
|
|
39
|
+
return deterministic_value(row)
|
|
40
|
+
else:
|
|
41
|
+
return probabilistic_value(row, logic_row)
|
|
42
|
+
|
|
43
|
+
Rule.formula(derive=models.MyTable.field, calling=ConditionalFormula)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Real Example (Supplier Selection)
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from logic.ai_requests.supplier_selection import get_supplier_price_from_ai
|
|
50
|
+
|
|
51
|
+
def ItemUnitPriceFromSupplier(row: models.Item, old_row, logic_row):
|
|
52
|
+
"""
|
|
53
|
+
Determine Item.unit_price:
|
|
54
|
+
- IF Product has NO suppliers → copy from Product.unit_price (deterministic)
|
|
55
|
+
- IF Product has suppliers → call AI to select optimal supplier (probabilistic)
|
|
56
|
+
"""
|
|
57
|
+
if row.product.count_suppliers == 0:
|
|
58
|
+
logic_row.log("Product has no suppliers, using product.unit_price")
|
|
59
|
+
return row.product.unit_price # Deterministic
|
|
60
|
+
|
|
61
|
+
# Probabilistic - AI selects best supplier
|
|
62
|
+
return get_supplier_price_from_ai(
|
|
63
|
+
row=row,
|
|
64
|
+
logic_row=logic_row,
|
|
65
|
+
candidates='product.ProductSupplierList',
|
|
66
|
+
optimize_for='fastest reliable delivery while keeping costs reasonable',
|
|
67
|
+
fallback='min:unit_cost'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Rule.formula(derive=models.Item.unit_price, calling=ItemUnitPriceFromSupplier)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Key Points:**
|
|
74
|
+
- Formula handles BOTH deterministic and probabilistic cases
|
|
75
|
+
- Clear condition determines which path to take
|
|
76
|
+
- AI handler encapsulates complexity
|
|
77
|
+
- Fallback strategy always provided
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Request Pattern for Audit Trail
|
|
82
|
+
|
|
83
|
+
Every AI computation creates an audit record capturing:
|
|
84
|
+
- Request details (what was asked)
|
|
85
|
+
- Response details (what AI decided)
|
|
86
|
+
- Reasoning (why AI made that choice)
|
|
87
|
+
- Timestamp (when decision was made)
|
|
88
|
+
|
|
89
|
+
### Audit Table Structure
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
class SysSupplierReq(Base):
|
|
93
|
+
"""Audit table for AI supplier selection"""
|
|
94
|
+
__tablename__ = 'sys_supplier_req'
|
|
95
|
+
|
|
96
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
97
|
+
item_id = Column(ForeignKey('item.id'))
|
|
98
|
+
product_id = Column(ForeignKey('product.id'), nullable=False)
|
|
99
|
+
|
|
100
|
+
# AI Response
|
|
101
|
+
chosen_supplier_id = Column(ForeignKey('supplier.id'))
|
|
102
|
+
chosen_unit_price = Column(DECIMAL)
|
|
103
|
+
reason = Column(String)
|
|
104
|
+
|
|
105
|
+
# Metadata
|
|
106
|
+
request = Column(String) # Optional: stores AI prompt
|
|
107
|
+
created_on = Column(DateTime, server_default=func.now())
|
|
108
|
+
|
|
109
|
+
# Relationships for navigation
|
|
110
|
+
item = relationship("Item", back_populates="SysSupplierReqList")
|
|
111
|
+
product = relationship("Product", back_populates="SysSupplierReqList")
|
|
112
|
+
supplier = relationship("Supplier", back_populates="SysSupplierReqList")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Implementation Pattern
|
|
116
|
+
|
|
117
|
+
**AI Handler (returns value):**
|
|
118
|
+
```python
|
|
119
|
+
def get_supplier_price_from_ai(row, logic_row, candidates, optimize_for, fallback):
|
|
120
|
+
"""
|
|
121
|
+
Creates audit record and returns computed value.
|
|
122
|
+
Event handler populates audit fields DURING this call.
|
|
123
|
+
"""
|
|
124
|
+
# Create audit record using LogicBank triggered insert
|
|
125
|
+
audit_logic_row = logic_row.new_logic_row(models.SysSupplierReq)
|
|
126
|
+
audit_record = audit_logic_row.row
|
|
127
|
+
audit_logic_row.link(to_parent=logic_row)
|
|
128
|
+
|
|
129
|
+
# Set request context
|
|
130
|
+
audit_record.product_id = row.product_id
|
|
131
|
+
audit_record.item_id = row.id
|
|
132
|
+
|
|
133
|
+
# Insert triggers event handler which populates chosen_* fields
|
|
134
|
+
audit_logic_row.insert(reason="Supplier AI Request")
|
|
135
|
+
|
|
136
|
+
# Return value populated by event handler
|
|
137
|
+
return audit_record.chosen_unit_price
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Event Handler (populates audit):**
|
|
141
|
+
```python
|
|
142
|
+
def supplier_id_from_ai(row: models.SysSupplierReq, old_row, logic_row):
|
|
143
|
+
"""
|
|
144
|
+
Fires DURING audit record insert.
|
|
145
|
+
Computes AI decision and populates audit fields.
|
|
146
|
+
"""
|
|
147
|
+
if not logic_row.is_inserted():
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Get candidates (suppliers for this product)
|
|
151
|
+
suppliers = row.product.ProductSupplierList
|
|
152
|
+
|
|
153
|
+
# AI computation (or fallback)
|
|
154
|
+
if not has_api_key():
|
|
155
|
+
min_supplier = min(suppliers, key=lambda s: s.unit_cost)
|
|
156
|
+
else:
|
|
157
|
+
# Load test context for INPUT conditions (world_conditions)
|
|
158
|
+
test_context = load_test_context()
|
|
159
|
+
world_conditions = test_context.get('world_conditions', 'normal conditions')
|
|
160
|
+
|
|
161
|
+
# Call AI service with world conditions
|
|
162
|
+
result = call_ai_service(
|
|
163
|
+
candidates=suppliers,
|
|
164
|
+
optimize_for='fastest reliable delivery',
|
|
165
|
+
world_conditions=world_conditions # Test context provides conditions, not outputs
|
|
166
|
+
)
|
|
167
|
+
min_supplier = result.chosen_supplier
|
|
168
|
+
|
|
169
|
+
# Populate audit fields
|
|
170
|
+
row.chosen_supplier_id = min_supplier.supplier_id
|
|
171
|
+
row.chosen_unit_price = min_supplier.unit_cost
|
|
172
|
+
row.reason = f"Selected supplier {min_supplier.supplier_id}: lowest cost at {min_supplier.unit_cost}"
|
|
173
|
+
|
|
174
|
+
def declare_logic():
|
|
175
|
+
"""Auto-discovery calls this to register event handler"""
|
|
176
|
+
Rule.early_row_event(on_class=models.SysSupplierReq, calling=supplier_id_from_ai)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Why This Works:**
|
|
180
|
+
1. Formula calls `get_supplier_price_from_ai()`
|
|
181
|
+
2. Handler creates audit record using `logic_row.insert()`
|
|
182
|
+
3. Insert triggers event handler `supplier_id_from_ai()`
|
|
183
|
+
4. Event handler computes AI decision and populates fields
|
|
184
|
+
5. Formula returns `audit_record.chosen_unit_price` (now populated)
|
|
185
|
+
6. All audit details captured in database
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Integration with Deterministic Rules
|
|
190
|
+
|
|
191
|
+
Probabilistic rules work seamlessly with deterministic rules:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# Deterministic rules execute first
|
|
195
|
+
Rule.sum(derive=models.Order.amount_total, as_sum_of=models.Item.amount)
|
|
196
|
+
Rule.formula(derive=models.Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
|
|
197
|
+
Rule.count(derive=models.Product.count_suppliers, as_count_of=models.ProductSupplier)
|
|
198
|
+
|
|
199
|
+
# Probabilistic rule depends on count_suppliers (deterministic)
|
|
200
|
+
def ItemUnitPriceFromSupplier(row, old_row, logic_row):
|
|
201
|
+
if row.product.count_suppliers == 0: # Uses deterministic count
|
|
202
|
+
return row.product.unit_price
|
|
203
|
+
return get_supplier_price_from_ai(...) # Probabilistic
|
|
204
|
+
|
|
205
|
+
Rule.formula(derive=models.Item.unit_price, calling=ItemUnitPriceFromSupplier)
|
|
206
|
+
|
|
207
|
+
# Constraint applies to final result (regardless of how computed)
|
|
208
|
+
Rule.constraint(validate=models.Customer,
|
|
209
|
+
as_condition=lambda row: row.balance <= row.credit_limit,
|
|
210
|
+
error_msg="Balance exceeds credit limit")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Execution Flow:**
|
|
214
|
+
1. Item inserted with product_id and quantity
|
|
215
|
+
2. count_suppliers computed (deterministic sum)
|
|
216
|
+
3. unit_price computed (conditional: deterministic OR probabilistic)
|
|
217
|
+
4. amount computed (deterministic: quantity * unit_price)
|
|
218
|
+
5. amount_total computed (deterministic sum)
|
|
219
|
+
6. balance updated (deterministic sum)
|
|
220
|
+
7. credit_limit constraint checked (deterministic validation)
|
|
221
|
+
|
|
222
|
+
**Key Insight:** AI decision (step 3) is just one step in the chain. All other rules remain deterministic.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Testing Probabilistic Rules
|
|
227
|
+
|
|
228
|
+
### Test Without AI (Deterministic Path)
|
|
229
|
+
```python
|
|
230
|
+
def test_no_suppliers_uses_product_price():
|
|
231
|
+
"""When product has no suppliers, should use product.unit_price"""
|
|
232
|
+
session = create_session()
|
|
233
|
+
|
|
234
|
+
# Setup: Product with NO suppliers
|
|
235
|
+
product = Product(id=1, name="Test", unit_price=100.00)
|
|
236
|
+
product.count_suppliers = 0 # Deterministic
|
|
237
|
+
|
|
238
|
+
# Test: Insert item
|
|
239
|
+
item = Item(product_id=1, quantity=5)
|
|
240
|
+
session.add(item)
|
|
241
|
+
session.flush()
|
|
242
|
+
|
|
243
|
+
# Verify: Used product.unit_price (deterministic path)
|
|
244
|
+
assert item.unit_price == 100.00
|
|
245
|
+
assert item.amount == 500.00
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Test With AI (Probabilistic Path)
|
|
249
|
+
```python
|
|
250
|
+
def test_with_suppliers_uses_ai():
|
|
251
|
+
"""When product has suppliers, should call AI"""
|
|
252
|
+
session = create_session()
|
|
253
|
+
|
|
254
|
+
# Setup: Product WITH suppliers
|
|
255
|
+
product = Product(id=2, name="Test", unit_price=100.00)
|
|
256
|
+
supplier1 = Supplier(id=1, name="Fast Inc")
|
|
257
|
+
supplier2 = Supplier(id=2, name="Cheap Co")
|
|
258
|
+
ProductSupplier(product_id=2, supplier_id=1, unit_cost=110.00)
|
|
259
|
+
ProductSupplier(product_id=2, supplier_id=2, unit_cost=95.00)
|
|
260
|
+
product.count_suppliers = 2 # Deterministic
|
|
261
|
+
|
|
262
|
+
# Test: Insert item
|
|
263
|
+
item = Item(product_id=2, quantity=5)
|
|
264
|
+
session.add(item)
|
|
265
|
+
session.flush()
|
|
266
|
+
|
|
267
|
+
# Verify: Used AI (probabilistic path)
|
|
268
|
+
assert item.unit_price in [110.00, 95.00] # One of the suppliers
|
|
269
|
+
|
|
270
|
+
# Verify: Audit record created
|
|
271
|
+
audit = session.query(SysSupplierReq).filter_by(item_id=item.id).one()
|
|
272
|
+
assert audit.chosen_supplier_id in [1, 2]
|
|
273
|
+
assert audit.chosen_unit_price in [110.00, 95.00]
|
|
274
|
+
assert audit.reason is not None
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Test AI Logic Independently
|
|
278
|
+
```python
|
|
279
|
+
@patch('logic.ai_requests.supplier_selection.call_ai_service')
|
|
280
|
+
def test_ai_handler_with_mock(mock_ai):
|
|
281
|
+
"""Test AI handler in isolation"""
|
|
282
|
+
mock_ai.return_value = {
|
|
283
|
+
'supplier_id': 2,
|
|
284
|
+
'unit_cost': 95.00,
|
|
285
|
+
'reason': 'Lowest cost'
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Create test context with INPUT conditions (not predetermined outputs)
|
|
289
|
+
# Example: test_context = {'world_conditions': 'Suez Canal blocked'}
|
|
290
|
+
row = create_test_item()
|
|
291
|
+
logic_row = create_test_logic_row(row)
|
|
292
|
+
|
|
293
|
+
# Call handler (AI will make decision based on world conditions)
|
|
294
|
+
result = get_supplier_price_from_ai(row, logic_row, ...)
|
|
295
|
+
|
|
296
|
+
# Verify AI was called with proper conditions
|
|
297
|
+
assert result == 95.00
|
|
298
|
+
assert mock_ai.called
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Common Patterns
|
|
304
|
+
|
|
305
|
+
### Pattern 1: Simple Conditional AI
|
|
306
|
+
```python
|
|
307
|
+
def my_formula(row, old_row, logic_row):
|
|
308
|
+
if has_simple_answer(row):
|
|
309
|
+
return simple_calculation(row)
|
|
310
|
+
return get_ai_value(row, logic_row, ...)
|
|
311
|
+
|
|
312
|
+
Rule.formula(derive=models.MyTable.field, calling=my_formula)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Pattern 2: AI with Fallback Strategy
|
|
316
|
+
```python
|
|
317
|
+
def robust_formula(row, old_row, logic_row):
|
|
318
|
+
try:
|
|
319
|
+
return get_ai_value(row, logic_row, fallback='min:cost')
|
|
320
|
+
except AIError:
|
|
321
|
+
return row.default_value
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Pattern 3: Multiple AI Decisions
|
|
325
|
+
```python
|
|
326
|
+
def complex_formula(row, old_row, logic_row):
|
|
327
|
+
# First decision: supplier
|
|
328
|
+
supplier_price = get_supplier_price_from_ai(...)
|
|
329
|
+
|
|
330
|
+
# Second decision: route
|
|
331
|
+
shipping_cost = get_route_cost_from_ai(...)
|
|
332
|
+
|
|
333
|
+
return supplier_price + shipping_cost
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Pattern 4: AI Influences Multiple Fields
|
|
337
|
+
```python
|
|
338
|
+
def ai_event_populates_multiple_fields(row, old_row, logic_row):
|
|
339
|
+
if logic_row.is_inserted():
|
|
340
|
+
result = call_ai_service(...)
|
|
341
|
+
row.field1 = result.value1
|
|
342
|
+
row.field2 = result.value2
|
|
343
|
+
row.field3 = result.value3
|
|
344
|
+
row.audit_reason = result.reasoning
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Best Practices
|
|
350
|
+
|
|
351
|
+
1. **Always provide fallback:** Never let AI failure break business logic
|
|
352
|
+
2. **Use conditional formulas:** Check simple cases before calling AI
|
|
353
|
+
3. **Create audit records:** Capture every AI decision for observability
|
|
354
|
+
4. **Test both paths:** Test deterministic path AND AI path independently
|
|
355
|
+
5. **Document AI goals:** What is AI optimizing for? Make it explicit
|
|
356
|
+
6. **Handle errors gracefully:** Catch exceptions, log details, use fallback
|
|
357
|
+
7. **Use early_row_event:** Populate audit fields before other rules need them
|
|
358
|
+
8. **Leverage relationships:** Navigate to candidates via SQLAlchemy relationships
|
|
359
|
+
9. **Keep handlers reusable:** One AI handler can serve multiple use cases
|
|
360
|
+
10. **Monitor audit tables:** Review AI decisions periodically for quality
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Troubleshooting
|
|
365
|
+
|
|
366
|
+
### Issue: AI not called when expected
|
|
367
|
+
**Check:**
|
|
368
|
+
- Is conditional logic correct? (`if count > 0` vs `if count == 0`)
|
|
369
|
+
- Are relationships loaded? (Use `row.product.ProductSupplierList`)
|
|
370
|
+
- Is count_suppliers computed before formula runs?
|
|
371
|
+
|
|
372
|
+
### Issue: Audit record empty or missing fields
|
|
373
|
+
**Check:**
|
|
374
|
+
- Is event handler registered? (Check `declare_logic()` called)
|
|
375
|
+
- Is event checking `logic_row.is_inserted()`?
|
|
376
|
+
- Is event handler using `early_row_event` (not `row_event`)?
|
|
377
|
+
|
|
378
|
+
### Issue: `request` and `reason` fields contain generic/incomplete data
|
|
379
|
+
**Problem:** Audit trail lacks actionable details for debugging/compliance
|
|
380
|
+
**Solution:** Populate fields in AI handler (where data exists) with complete information:
|
|
381
|
+
```python
|
|
382
|
+
# ✅ CORRECT - In AI handler, populate with full context
|
|
383
|
+
candidate_summary = ', '.join([f"{s.supplier.name}(${s.unit_cost})" for s in suppliers])
|
|
384
|
+
row.request = f"Select supplier for {product.name}: Candidates=[{candidate_summary}], World={world}"
|
|
385
|
+
row.reason = f"AI: {supplier_name} (${price}) - {ai_explanation}"
|
|
386
|
+
|
|
387
|
+
# ❌ WRONG - Generic constants with no business context
|
|
388
|
+
row.request = "Select supplier"
|
|
389
|
+
row.reason = "AI selection"
|
|
390
|
+
```
|
|
391
|
+
**Key:** Include supplier names, prices, world conditions - not just IDs
|
|
392
|
+
|
|
393
|
+
### Issue: "AttributeError: 'NoneType' object has no attribute 'product_id'" on delete
|
|
394
|
+
**Problem:** Early events fire on delete but `old_row` is None
|
|
395
|
+
**Solution:** Check `is_deleted()` FIRST before accessing `old_row`:
|
|
396
|
+
```python
|
|
397
|
+
def my_early_event(row, old_row, logic_row):
|
|
398
|
+
# ✅ CORRECT - Check delete first
|
|
399
|
+
if logic_row.is_deleted():
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
# Now safe to access old_row
|
|
403
|
+
if row.product_id != old_row.product_id:
|
|
404
|
+
# handle change
|
|
405
|
+
pass
|
|
406
|
+
```
|
|
407
|
+
**Rule:** ALL early events that access `old_row` MUST check `is_deleted()` first
|
|
408
|
+
|
|
409
|
+
### Issue: "Session is already flushing" error
|
|
410
|
+
**Solution:** Use LogicBank triggered insert pattern:
|
|
411
|
+
```python
|
|
412
|
+
# ❌ WRONG
|
|
413
|
+
session.add(audit)
|
|
414
|
+
session.flush()
|
|
415
|
+
|
|
416
|
+
# ✅ CORRECT
|
|
417
|
+
audit_logic_row = logic_row.new_logic_row(models.Audit)
|
|
418
|
+
audit_logic_row.insert(reason="AI")
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Issue: Audit record created but value not returned
|
|
422
|
+
**Check:**
|
|
423
|
+
- Does event handler populate fields BEFORE formula returns?
|
|
424
|
+
- Is event handler using `early_row_event` (runs during insert)?
|
|
425
|
+
- Does formula return value from audit record? (`return audit_record.field`)
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Test Context: Input Conditions vs Output Mocking
|
|
430
|
+
|
|
431
|
+
**CRITICAL DISTINCTION:** Test context provides INPUT CONDITIONS for AI, NOT predetermined outputs.
|
|
432
|
+
|
|
433
|
+
**Purpose:**
|
|
434
|
+
- Test how AI responds to different scenarios (e.g., "Suez Canal blocked")
|
|
435
|
+
- Verify AI considers world conditions in its decision-making
|
|
436
|
+
- Enable repeatable testing with varying conditions
|
|
437
|
+
|
|
438
|
+
**Example Test Context (config/ai_test_context.yaml):**
|
|
439
|
+
```yaml
|
|
440
|
+
# ✅ CORRECT - Provides input conditions
|
|
441
|
+
world_conditions: "Suez Canal blocked, alternate routes required"
|
|
442
|
+
|
|
443
|
+
# ❌ WRONG - Predetermines outputs (defeats AI testing)
|
|
444
|
+
# selected_supplier_id: 2 # Don't do this!
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**How It Works:**
|
|
448
|
+
1. Load test context for `world_conditions`
|
|
449
|
+
2. Pass conditions to AI prompt
|
|
450
|
+
3. AI makes decision based on those conditions
|
|
451
|
+
4. Verify AI selected appropriate supplier given the conditions
|
|
452
|
+
|
|
453
|
+
**Testing Strategy:**
|
|
454
|
+
- **Normal conditions:** AI should optimize for cost
|
|
455
|
+
- **Disrupted conditions:** AI should prioritize reliability/alternate routes
|
|
456
|
+
- **No API key:** System uses fallback (min cost)
|
|
457
|
+
|
|
458
|
+
**Key Insight:** Test context lets you verify AI adapts to different scenarios WITHOUT mocking the AI itself.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Summary
|
|
463
|
+
|
|
464
|
+
**Probabilistic Logic Pattern:**
|
|
465
|
+
1. Conditional formula decides deterministic vs AI
|
|
466
|
+
2. AI handler creates audit record using triggered insert
|
|
467
|
+
3. Event handler populates audit fields with AI decision
|
|
468
|
+
4. Formula returns value from audit record
|
|
469
|
+
5. All audit details captured for observability
|
|
470
|
+
|
|
471
|
+
**Key Benefits:**
|
|
472
|
+
- Seamless integration with deterministic rules
|
|
473
|
+
- Full audit trail of AI decisions
|
|
474
|
+
- Test context for scenario-based testing
|
|
475
|
+
- Graceful fallback when AI unavailable
|
|
476
|
+
- Testable at multiple levels
|
|
477
|
+
- Reusable AI handlers across use cases
|
|
478
|
+
|
|
479
|
+
**Remember:**
|
|
480
|
+
- Probabilistic rules are still rules - they participate in multi-table transactions
|
|
481
|
+
- AI decisions are atomic with database updates
|
|
482
|
+
- Audit trails enable debugging and quality monitoring
|
|
483
|
+
- Fallback strategies ensure business continuity
|