ApiLogicServer 15.4.3__py3-none-any.whl → 16.0.0__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 +228 -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 +22 -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 +1074 -0
- api_logic_server_cli/prototypes/base/docs/training/probabilistic_logic_guide.md +444 -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 +326 -142
- api_logic_server_cli/prototypes/basic_demo/.github/welcome.md +15 -1
- 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/samples/basic_demo_sample/.github/.copilot-instructions.md +502 -76
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/METADATA +1 -1
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/RECORD +26 -18
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/WHEEL +0 -0
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/entry_points.txt +0 -0
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TEMPLATE: Probabilistic + Deterministic Rules Implementation
|
|
3
|
+
|
|
4
|
+
This template provides a clean reference implementation for AI value computation
|
|
5
|
+
alongside deterministic rules, using the Request Pattern with early events.
|
|
6
|
+
|
|
7
|
+
Pattern: Early event with wrapper function that returns populated request object
|
|
8
|
+
version: 3.1 - proper initialization of ai_request data
|
|
9
|
+
date: November 21, 2025
|
|
10
|
+
source: docs/training/probabilistic_template.py
|
|
11
|
+
|
|
12
|
+
See docs/training/probabilistic_logic.prompt for complete documentation.
|
|
13
|
+
|
|
14
|
+
IMPORTANT: When generating code from this template, include version tracking
|
|
15
|
+
in generated files (supplier_selection.py, check_credit.py) with:
|
|
16
|
+
version: 3.0
|
|
17
|
+
date: [current date]
|
|
18
|
+
source: docs/training/probabilistic_logic.prompt
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import database.models as models
|
|
22
|
+
|
|
23
|
+
def declare_logic():
|
|
24
|
+
from logic_bank.logic_bank import Rule
|
|
25
|
+
"""
|
|
26
|
+
Declarative rules combining deterministic and probabilistic logic.
|
|
27
|
+
|
|
28
|
+
Deterministic: constraints, sums, formulas, counts
|
|
29
|
+
Probabilistic: AI-driven value computation via early events
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Deterministic Rules
|
|
33
|
+
Rule.constraint(validate=models.Customer,
|
|
34
|
+
as_condition=lambda row: row.balance <= row.credit_limit,
|
|
35
|
+
error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
|
|
36
|
+
|
|
37
|
+
Rule.sum(derive=models.Customer.balance, as_sum_of=models.Order.amount_total,
|
|
38
|
+
where=lambda row: row.date_shipped is None)
|
|
39
|
+
|
|
40
|
+
Rule.sum(derive=models.Order.amount_total, as_sum_of=models.Item.amount)
|
|
41
|
+
|
|
42
|
+
Rule.formula(derive=models.Item.amount, as_expression=lambda row: row.quantity * row.unit_price)
|
|
43
|
+
|
|
44
|
+
Rule.count(derive=models.Product.count_suppliers, as_count_of=models.ProductSupplier)
|
|
45
|
+
|
|
46
|
+
# Probabilistic Rule - Early event on Item
|
|
47
|
+
Rule.early_row_event(on_class=models.Item, calling=set_item_unit_price_from_supplier)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def set_item_unit_price_from_supplier(row: models.Item, old_row: models.Item, logic_row):
|
|
51
|
+
"""
|
|
52
|
+
Early event: Sets unit_price using AI if suppliers exist, else copy from product.
|
|
53
|
+
|
|
54
|
+
Fires on insert AND when product_id changes (same semantics as copy rule).
|
|
55
|
+
|
|
56
|
+
Pattern:
|
|
57
|
+
1. Check condition (suppliers available?)
|
|
58
|
+
2. Call wrapper function
|
|
59
|
+
3. Extract needed value from returned object
|
|
60
|
+
"""
|
|
61
|
+
from logic.logic_discovery.ai_requests.supplier_selection import get_supplier_selection_from_ai
|
|
62
|
+
|
|
63
|
+
# Skip on delete (old_row is None) - CRITICAL: Check this FIRST
|
|
64
|
+
if logic_row.is_deleted():
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Process on insert OR when product_id changes
|
|
68
|
+
if not (logic_row.is_inserted() or row.product_id != old_row.product_id):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
product = row.product
|
|
72
|
+
|
|
73
|
+
# No suppliers - use product's default price
|
|
74
|
+
if product.count_suppliers == 0:
|
|
75
|
+
row.unit_price = product.unit_price
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Product has suppliers - call wrapper
|
|
79
|
+
supplier_req = get_supplier_selection_from_ai(
|
|
80
|
+
product_id=row.product_id,
|
|
81
|
+
item_id=row.id,
|
|
82
|
+
logic_row=logic_row
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Extract value
|
|
86
|
+
row.unit_price = supplier_req.chosen_unit_price
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
AI HANDLER IMPLEMENTATION
|
|
91
|
+
Location: logic/logic_discovery/ai_requests/supplier_selection.py
|
|
92
|
+
|
|
93
|
+
This module contains:
|
|
94
|
+
1. declare_logic() - Registers early event on SysSupplierReq
|
|
95
|
+
2. select_supplier_via_ai() - AI handler that implements supplier selection
|
|
96
|
+
3. get_supplier_selection_from_ai() - Wrapper that hides Request Pattern
|
|
97
|
+
|
|
98
|
+
version: 3.0
|
|
99
|
+
date: November 21, 2025
|
|
100
|
+
source: docs/training/probabilistic_template.py
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
from logic_bank.exec_row_logic.logic_row import LogicRow
|
|
104
|
+
from logic_bank.logic_bank import Rule
|
|
105
|
+
from database import models
|
|
106
|
+
from decimal import Decimal
|
|
107
|
+
import os
|
|
108
|
+
|
|
109
|
+
def declare_logic():
|
|
110
|
+
"""
|
|
111
|
+
Register early event on SysSupplierReq to populate chosen_* fields via AI.
|
|
112
|
+
|
|
113
|
+
This Request Pattern approach provides full audit trails and separation of concerns.
|
|
114
|
+
See: https://apilogicserver.github.io/Docs/Logic/#rule-patterns
|
|
115
|
+
"""
|
|
116
|
+
Rule.early_row_event(on_class=models.SysSupplierReq, calling=select_supplier_via_ai)
|
|
117
|
+
|
|
118
|
+
def select_supplier_via_ai(row: models.SysSupplierReq, old_row, logic_row: LogicRow):
|
|
119
|
+
"""
|
|
120
|
+
Early event (called via insert from wrapper) to populate chosen_* fields via AI.
|
|
121
|
+
|
|
122
|
+
This AI handler gets called automatically when SysSupplierReq is inserted,
|
|
123
|
+
populating AI Results: chosen_supplier_id and chosen_unit_price.
|
|
124
|
+
"""
|
|
125
|
+
if not logic_row.is_inserted():
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# Get candidates (suppliers for this product)
|
|
129
|
+
product = row.product
|
|
130
|
+
suppliers = product.ProductSupplierList if product else []
|
|
131
|
+
|
|
132
|
+
if not suppliers:
|
|
133
|
+
row.request = f"Select supplier for {product.name if product else 'unknown product'} - No suppliers available"
|
|
134
|
+
row.reason = "No suppliers exist for this product"
|
|
135
|
+
logic_row.log("No suppliers available for AI selection")
|
|
136
|
+
row.fallback_used = True
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Check for test context first (BEFORE API key check)
|
|
140
|
+
from pathlib import Path
|
|
141
|
+
import yaml
|
|
142
|
+
|
|
143
|
+
config_dir = Path(__file__).resolve().parent.parent.parent.parent / 'config'
|
|
144
|
+
context_file = config_dir / 'ai_test_context.yaml'
|
|
145
|
+
|
|
146
|
+
selected_supplier = None
|
|
147
|
+
|
|
148
|
+
if context_file.exists():
|
|
149
|
+
with open(str(context_file), 'r') as f:
|
|
150
|
+
test_context = yaml.safe_load(f)
|
|
151
|
+
if test_context and 'selected_supplier_id' in test_context:
|
|
152
|
+
supplier_id = test_context['selected_supplier_id']
|
|
153
|
+
selected_supplier = next((s for s in suppliers if s.supplier_id == supplier_id), None)
|
|
154
|
+
if selected_supplier:
|
|
155
|
+
candidate_summary = ', '.join([f"{s.supplier.name if s.supplier else 'Unknown'}(${s.unit_cost})" for s in suppliers])
|
|
156
|
+
world = test_context.get('world_conditions', 'normal conditions')
|
|
157
|
+
row.request = f"Select supplier for {product.name}: Candidates=[{candidate_summary}], World={world}"
|
|
158
|
+
row.reason = f"TEST MODE: Selected {selected_supplier.supplier.name if selected_supplier.supplier else 'supplier'} (${selected_supplier.unit_cost}) - world: {world}"
|
|
159
|
+
logic_row.log(f"Using test context: supplier {supplier_id}")
|
|
160
|
+
row.fallback_used = False
|
|
161
|
+
|
|
162
|
+
# If no test context, try AI (check for API key)
|
|
163
|
+
if not selected_supplier:
|
|
164
|
+
api_key = os.getenv("APILOGICSERVER_CHATGPT_APIKEY")
|
|
165
|
+
if api_key:
|
|
166
|
+
try:
|
|
167
|
+
# Call OpenAI API with structured prompt
|
|
168
|
+
from openai import OpenAI
|
|
169
|
+
import json
|
|
170
|
+
|
|
171
|
+
client = OpenAI(api_key=api_key)
|
|
172
|
+
|
|
173
|
+
# Build candidate data for prompt
|
|
174
|
+
candidate_data = []
|
|
175
|
+
for supplier in suppliers:
|
|
176
|
+
candidate_data.append({
|
|
177
|
+
'supplier_id': supplier.supplier_id,
|
|
178
|
+
'supplier_name': supplier.supplier.name if supplier.supplier else 'Unknown',
|
|
179
|
+
'unit_cost': float(supplier.unit_cost) if supplier.unit_cost else 0.0,
|
|
180
|
+
'lead_time_days': supplier.lead_time_days if hasattr(supplier, 'lead_time_days') else None
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
world_conditions = test_context.get('world_conditions', 'normal conditions') if 'test_context' in locals() else 'normal conditions'
|
|
184
|
+
|
|
185
|
+
prompt = f"""
|
|
186
|
+
You are a supply chain optimization expert. Select the best supplier from the candidates below.
|
|
187
|
+
|
|
188
|
+
World Conditions: {world_conditions}
|
|
189
|
+
|
|
190
|
+
Optimization Goal: fastest reliable delivery while keeping costs reasonable
|
|
191
|
+
|
|
192
|
+
Candidates:
|
|
193
|
+
{yaml.dump(candidate_data, default_flow_style=False)}
|
|
194
|
+
|
|
195
|
+
Respond with ONLY valid JSON in this exact format (no markdown, no code blocks):
|
|
196
|
+
{{
|
|
197
|
+
"chosen_supplier_id": <id>,
|
|
198
|
+
"chosen_unit_price": <price>,
|
|
199
|
+
"reason": "<brief explanation>"
|
|
200
|
+
}}
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
# Populate request field with actual prompt summary
|
|
204
|
+
candidate_list = ', '.join([c['supplier_name'] + '($' + str(c['unit_cost']) + ')' for c in candidate_data])
|
|
205
|
+
row.request = f"AI Prompt: Product={product.name}, World={world_conditions}, Candidates={len(candidate_data)}: {candidate_list}"
|
|
206
|
+
|
|
207
|
+
logic_row.log(f"Calling OpenAI API with {len(candidate_data)} candidates")
|
|
208
|
+
|
|
209
|
+
response = client.chat.completions.create(
|
|
210
|
+
model="gpt-4o-2024-08-06",
|
|
211
|
+
messages=[
|
|
212
|
+
{"role": "system", "content": "You are a supply chain expert. Respond with valid JSON only."},
|
|
213
|
+
{"role": "user", "content": prompt}
|
|
214
|
+
],
|
|
215
|
+
temperature=0.7
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
response_text = response.choices[0].message.content.strip()
|
|
219
|
+
logic_row.log(f"OpenAI response: {response_text}")
|
|
220
|
+
|
|
221
|
+
# Parse JSON response
|
|
222
|
+
ai_result = json.loads(response_text)
|
|
223
|
+
|
|
224
|
+
# Find the selected supplier
|
|
225
|
+
selected_supplier = next((s for s in suppliers if s.supplier_id == ai_result['chosen_supplier_id']), None)
|
|
226
|
+
if selected_supplier:
|
|
227
|
+
supplier_name = selected_supplier.supplier.name if selected_supplier.supplier else 'Unknown'
|
|
228
|
+
row.reason = f"AI: {supplier_name} (${selected_supplier.unit_cost}) - {ai_result.get('reason', 'No reason provided')}"
|
|
229
|
+
row.fallback_used = False
|
|
230
|
+
else:
|
|
231
|
+
logic_row.log(f"AI selected invalid supplier_id {ai_result['chosen_supplier_id']}, using fallback")
|
|
232
|
+
selected_supplier = min(suppliers, key=lambda s: float(s.unit_cost) if s.unit_cost else 999999.0)
|
|
233
|
+
fallback_name = selected_supplier.supplier.name if selected_supplier.supplier else 'Unknown'
|
|
234
|
+
row.reason = f"Fallback: {fallback_name} (${selected_supplier.unit_cost}) - AI returned invalid supplier"
|
|
235
|
+
row.fallback_used = True
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logic_row.log(f"OpenAI API error: {e}, using fallback")
|
|
239
|
+
selected_supplier = min(suppliers, key=lambda s: float(s.unit_cost) if s.unit_cost else 999999.0)
|
|
240
|
+
fallback_name = selected_supplier.supplier.name if selected_supplier.supplier else 'Unknown'
|
|
241
|
+
candidate_summary = ', '.join([f"{s.supplier.name if s.supplier else 'Unknown'}(${s.unit_cost})" for s in suppliers])
|
|
242
|
+
row.request = f"Select supplier for {product.name}: Candidates=[{candidate_summary}] - API ERROR"
|
|
243
|
+
row.reason = f"Fallback: {fallback_name} (${selected_supplier.unit_cost}) - API error: {str(e)[:100]}"
|
|
244
|
+
row.fallback_used = True
|
|
245
|
+
else:
|
|
246
|
+
# No API key - use fallback strategy (min cost)
|
|
247
|
+
logic_row.log("No API key, using fallback: minimum cost")
|
|
248
|
+
selected_supplier = min(suppliers, key=lambda s: float(s.unit_cost) if s.unit_cost else 999999.0)
|
|
249
|
+
fallback_name = selected_supplier.supplier.name if selected_supplier.supplier else 'Unknown'
|
|
250
|
+
candidate_summary = ', '.join([f"{s.supplier.name if s.supplier else 'Unknown'}(${s.unit_cost})" for s in suppliers])
|
|
251
|
+
row.request = f"Select supplier for {product.name}: Candidates=[{candidate_summary}] - NO API KEY"
|
|
252
|
+
row.reason = f"Fallback: {fallback_name} (${selected_supplier.unit_cost}) - minimum cost (no API key)"
|
|
253
|
+
row.fallback_used = True
|
|
254
|
+
|
|
255
|
+
# Populate AI results
|
|
256
|
+
if selected_supplier:
|
|
257
|
+
row.chosen_supplier_id = int(selected_supplier.supplier_id) # Must be int for SQLite FK
|
|
258
|
+
row.chosen_unit_price = selected_supplier.unit_cost
|
|
259
|
+
logic_row.log(f"Selected supplier {selected_supplier.supplier_id} with price {selected_supplier.unit_cost}")
|
|
260
|
+
|
|
261
|
+
def get_supplier_selection_from_ai(product_id: int, item_id: int, logic_row: LogicRow) -> models.SysSupplierReq:
|
|
262
|
+
"""
|
|
263
|
+
Typically called from Item (Receiver) early event
|
|
264
|
+
to get AI results from chosen ProductSupplier (Provider).
|
|
265
|
+
|
|
266
|
+
See: https://apilogicserver.github.io/Docs/Logic-Using-AI/
|
|
267
|
+
|
|
268
|
+
1. Creates SysSupplierReq and inserts it (triggering AI event that populates chosen_* fields)
|
|
269
|
+
|
|
270
|
+
This wrapper hides Request Pattern implementation details.
|
|
271
|
+
See https://apilogicserver.github.io/Docs/Logic/#rule-patterns.
|
|
272
|
+
|
|
273
|
+
Returns populated SysSupplierReq object with:
|
|
274
|
+
- Standard AI Audit: request, reason, created_on, fallback_used
|
|
275
|
+
- Parent Context Links: item_id, product_id
|
|
276
|
+
- AI Results: chosen_supplier_id, chosen_unit_price
|
|
277
|
+
"""
|
|
278
|
+
# 1. Create request row using parent's logic_row
|
|
279
|
+
supplier_req_logic_row = logic_row.new_logic_row(models.SysSupplierReq)
|
|
280
|
+
supplier_req = supplier_req_logic_row.row
|
|
281
|
+
|
|
282
|
+
# 2. Set parent context (FK links)
|
|
283
|
+
supplier_req.product_id = product_id
|
|
284
|
+
supplier_req.item_id = item_id
|
|
285
|
+
|
|
286
|
+
# 3. Insert triggers early event which populates AI values
|
|
287
|
+
supplier_req_logic_row.insert(reason="AI supplier selection request")
|
|
288
|
+
|
|
289
|
+
# 4. Log filled request object for visibility
|
|
290
|
+
logic_row.log(f"AI Request: {supplier_req.request}")
|
|
291
|
+
logic_row.log(f"AI Results: supplier_id={supplier_req.chosen_supplier_id}, price={supplier_req.chosen_unit_price}, reason={supplier_req.reason}")
|
|
292
|
+
|
|
293
|
+
# 5. Return populated object (chosen_* fields now set by AI)
|
|
294
|
+
return supplier_req
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
KEY PATTERNS SUMMARY
|
|
299
|
+
|
|
300
|
+
1. EARLY EVENT PATTERN
|
|
301
|
+
- Register on receiver: Rule.early_row_event(on_class=models.Item, ...)
|
|
302
|
+
- Ensures AI executes before other rules
|
|
303
|
+
- Event calls wrapper, extracts values
|
|
304
|
+
|
|
305
|
+
2. WRAPPER FUNCTION
|
|
306
|
+
- Hides Request Pattern complexity
|
|
307
|
+
- Takes simple parameters: product_id, item_id, logic_row
|
|
308
|
+
- Returns populated request object
|
|
309
|
+
|
|
310
|
+
3. REQUEST PATTERN
|
|
311
|
+
- Create: logic_row.new_logic_row(models.SysXxxReq)
|
|
312
|
+
- Access: req = req_logic_row.row
|
|
313
|
+
- Context: req.product_id = product_id
|
|
314
|
+
- Trigger: req_logic_row.insert(reason="...")
|
|
315
|
+
- Return: return req
|
|
316
|
+
|
|
317
|
+
4. VALUE EXTRACTION
|
|
318
|
+
- Caller extracts what it needs
|
|
319
|
+
- Single value: row.unit_price = req.chosen_unit_price
|
|
320
|
+
- Multiple values: Extract multiple fields from same object
|
|
321
|
+
|
|
322
|
+
5. AUTO-DISCOVERY
|
|
323
|
+
- logic_discovery/ scanned recursively
|
|
324
|
+
- ai_requests/ subdirectory auto-discovered
|
|
325
|
+
- Each module's declare_logic() called automatically
|
|
326
|
+
"""
|
|
@@ -12,16 +12,15 @@ def discover_logic():
|
|
|
12
12
|
logic = []
|
|
13
13
|
logic_path = Path(__file__).parent
|
|
14
14
|
for root, dirs, files in os.walk(logic_path):
|
|
15
|
+
root_path = Path(root) # Get actual subdirectory path
|
|
15
16
|
for file in files:
|
|
16
|
-
if file.endswith(".py"):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
spec.loader.exec_module(each_logic_file) # runs "bare" module code (e.g., initialization)
|
|
24
|
-
each_logic_file.declare_logic() # invoke create function
|
|
17
|
+
if file.endswith(".py") and file not in ["auto_discovery.py", "__init__.py"]:
|
|
18
|
+
file_path = root_path / file # Build complete path
|
|
19
|
+
spec = importlib.util.spec_from_file_location("module.name", file_path)
|
|
20
|
+
logic.append(file)
|
|
21
|
+
each_logic_file = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(each_logic_file) # runs "bare" module code (e.g., initialization)
|
|
23
|
+
each_logic_file.declare_logic() # invoke create function
|
|
25
24
|
|
|
26
25
|
# if False and Path(__file__).parent.parent.parent.joinpath("docs/project_is_genai_demo.txt").exists():
|
|
27
26
|
# return # for genai_demo, logic is in logic/declare_logic.py (so ignore logic_discovery)
|