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.
Files changed (26) hide show
  1. api_logic_server_cli/add_cust/add_cust.py +6 -2
  2. api_logic_server_cli/api_logic_server.py +2 -1
  3. api_logic_server_cli/database/basic_demo.sqlite +0 -0
  4. api_logic_server_cli/prototypes/base/.github/.copilot-instructions.md +228 -76
  5. api_logic_server_cli/prototypes/base/docs/training/OVERVIEW.md +64 -0
  6. api_logic_server_cli/prototypes/base/docs/training/README.md +140 -0
  7. api_logic_server_cli/prototypes/base/docs/training/genai_logic_patterns.md +443 -0
  8. api_logic_server_cli/prototypes/base/docs/training/logic_bank_api.prompt +22 -0
  9. api_logic_server_cli/prototypes/base/docs/training/logic_bank_patterns.prompt +445 -0
  10. api_logic_server_cli/prototypes/base/docs/training/probabilistic_logic.prompt +1074 -0
  11. api_logic_server_cli/prototypes/base/docs/training/probabilistic_logic_guide.md +444 -0
  12. api_logic_server_cli/prototypes/base/docs/training/probabilistic_template.py +326 -0
  13. api_logic_server_cli/prototypes/base/logic/logic_discovery/auto_discovery.py +8 -9
  14. api_logic_server_cli/prototypes/basic_demo/.github/.copilot-instructions.md +326 -142
  15. api_logic_server_cli/prototypes/basic_demo/.github/welcome.md +15 -1
  16. api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
  17. api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
  18. api_logic_server_cli/prototypes/manager/.github/.copilot-instructions.md +61 -155
  19. api_logic_server_cli/prototypes/manager/.github/welcome.md +43 -0
  20. api_logic_server_cli/prototypes/manager/samples/basic_demo_sample/.github/.copilot-instructions.md +502 -76
  21. {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/METADATA +1 -1
  22. {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/RECORD +26 -18
  23. {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/WHEEL +0 -0
  24. {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/entry_points.txt +0 -0
  25. {apilogicserver-15.4.3.dist-info → apilogicserver-16.0.0.dist-info}/licenses/LICENSE +0 -0
  26. {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
- 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
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)