ApiLogicServer 14.5.4__py3-none-any.whl → 14.5.14__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 (56) hide show
  1. api_logic_server_cli/api_logic_server.py +4 -122
  2. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  3. api_logic_server_cli/create_from_model/__pycache__/api_logic_server_utils.cpython-312.pyc +0 -0
  4. api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
  5. api_logic_server_cli/create_from_model/api_logic_server_utils.py +29 -10
  6. api_logic_server_cli/create_from_model/dbml.py +16 -12
  7. api_logic_server_cli/database/basic_demo.sqlite +0 -0
  8. api_logic_server_cli/genai/genai.py +4 -0
  9. api_logic_server_cli/genai/genai_svcs.py +10 -2
  10. api_logic_server_cli/manager.py +3 -1
  11. api_logic_server_cli/prototypes/base/.vscode/launch.json +40 -0
  12. api_logic_server_cli/prototypes/base/api/api_discovery/mcp_discovery.py +58 -0
  13. api_logic_server_cli/prototypes/base/database/system/SAFRSBaseX.py +68 -2
  14. api_logic_server_cli/prototypes/base/database/system/SAFRSBaseX.pyZ +73 -0
  15. api_logic_server_cli/prototypes/{basic_demo/customizations/integration → base/integration/mcp}/.DS_Store +0 -0
  16. api_logic_server_cli/prototypes/base/integration/mcp/README_mcp.md +15 -0
  17. api_logic_server_cli/prototypes/{basic_demo/customizations → base}/integration/mcp/mcp_client_executor.py +91 -103
  18. api_logic_server_cli/prototypes/base/integration/mcp/mcp_schema.txt +47 -0
  19. api_logic_server_cli/prototypes/base/integration/mcp/mcp_server_discovery.json +9 -0
  20. api_logic_server_cli/prototypes/base/integration/mcp/test_notes.txt +37 -0
  21. api_logic_server_cli/prototypes/basic_demo/README.md +251 -91
  22. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/mcp_discovery.py +1 -1
  23. api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
  24. api_logic_server_cli/prototypes/basic_demo/customizations/database/models.py +27 -12
  25. api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +1 -1
  26. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/Zmcp_client_executor.py +294 -0
  27. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_tool_context.json +25 -0
  28. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/test_notes.txt +37 -0
  29. api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +1 -20
  30. api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/email_request.py +47 -0
  31. api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/mcp_client_executor_request.py +320 -0
  32. api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/simple_constraints.py +25 -0
  33. api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/admin.yaml +39 -32
  34. api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
  35. api_logic_server_cli/prototypes/basic_demo/iteration/ui/admin/admin.yaml +39 -44
  36. api_logic_server_cli/prototypes/manager/.vscode/launch.json +21 -0
  37. api_logic_server_cli/prototypes/manager/{README.md → READMEz.md} +4 -4
  38. api_logic_server_cli/prototypes/manager/REAMDE.md +1057 -0
  39. api_logic_server_cli/prototypes/manager/system/genai/mcp_learning/mcp.prompt +27 -0
  40. api_logic_server_cli/prototypes/manager/system/install-ApiLogicServer-dev/install-ApiLogicServer-dev.sh +2 -1
  41. {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.14.dist-info}/METADATA +1 -1
  42. {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.14.dist-info}/RECORD +46 -42
  43. {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.14.dist-info}/WHEEL +1 -1
  44. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/3_executor_test_agent.py +0 -52
  45. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/README_functon.md +0 -201
  46. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/ai_plugin.json +0 -17
  47. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/nw-swagger_3.json +0 -1731
  48. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/snippets.txt +0 -5
  49. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3 genai_demo_with_get.json +0 -1731
  50. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3.json +0 -1782
  51. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo.json +0 -264
  52. api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo_with_update.json +0 -1782
  53. api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/auto_discovery.py +0 -52
  54. {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.14.dist-info}/entry_points.txt +0 -0
  55. {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.14.dist-info}/licenses/LICENSE +0 -0
  56. {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,294 @@
1
+ """
2
+ This simulates the MCP Client Executor,
3
+ which takes a natural language query and converts it into a tool context block:
4
+
5
+ 1. Discovers MCP servers (from config)
6
+ 2. Queries OpenAI's GPT-4 model to obtain the tool context based on a provided schema and a natural language query
7
+ 3. Processes the tool context (calls the indicated MCP (als) endpoints)
8
+
9
+ Notes:
10
+ * See: integration/mcp/README_mcp.md
11
+ * python api_logic_server_run.py
12
+
13
+ """
14
+
15
+ import json
16
+ import os, sys
17
+ from typing import Dict, List
18
+ import openai
19
+ import requests
20
+ from logic_bank.logic_bank import Rule
21
+ from logic_bank.util import ConstraintException
22
+
23
+ # Set your OpenAI API key
24
+ openai.api_key = os.getenv("APILOGICSERVER_CHATGPT_APIKEY")
25
+
26
+ server_url = os.getenv("APILOGICSERVER_URL", "http://localhost:5656/api")
27
+
28
+ # debug settings
29
+ test_type = 'orchestration' # 'simple_get' or 'orchestration'
30
+ create_tool_context_from_llm = True
31
+ ''' set to False to bypass LLM call and save 2-3 secs in testing '''
32
+ use_test_schema = False
33
+ ''' True means bypass discovery, use hard-coded schedma file '''
34
+
35
+ def discover_mcp_servers():
36
+ """ Discover the MCP servers by calling the /api/.well-known/mcp.json endpoint.
37
+ This function retrieves the list of available MCP servers and their capabilities.
38
+ """
39
+ global server_url, use_test_schema
40
+
41
+ # create schema_text (for prompt), by reading integration/mcp/mcp_schema.txt
42
+
43
+ # find the servers - read the mcp_server_discovery.json file
44
+ discovery_file_path = os.path.join(os.path.dirname(__file__), "../../integration/mcp/mcp_server_discovery.json")
45
+ try:
46
+ with open(discovery_file_path, "r") as discovery_file:
47
+ discovery_data = json.load(discovery_file)
48
+ print(f"\n1. Discovered MCP servers from config file: {discovery_file_path}:" + json.dumps(discovery_data, indent=4))
49
+ except FileNotFoundError:
50
+ print(f"Discovery file not found at {discovery_file_path}.")
51
+ except json.JSONDecodeError as e:
52
+ print(f"Error decoding JSON from {discovery_file_path}: {e}")
53
+
54
+ for each_server in discovery_data["servers"]:
55
+ discovery_url = each_server["schema_url"]
56
+
57
+ # Call the discovery_url to get the MCP/API schema
58
+ try:
59
+ response = requests.get(discovery_url)
60
+ if response.status_code == 200:
61
+ api_schema = response.json()
62
+ print()
63
+ request_print = json.dumps(api_schema, indent=4)[0:400] + '\n... etc' # limit for readability
64
+ print(f"\n\nAPI Schema from discovery schema_url: {discovery_url}:\n" + request_print)
65
+ else:
66
+ print(f"Failed to retrieve API schema from {discovery_url}: {response.status_code}")
67
+ except requests.RequestException as e:
68
+ print(f"Error calling OpenAPI URL: {e}")
69
+ return json.dumps(api_schema)
70
+
71
+
72
+ def get_user_nl_query_and_training(query: str):
73
+ """ Get the natural language query from the user.
74
+ Add training for the LLM to generate a tool context block.
75
+
76
+ """
77
+
78
+ global test_type
79
+ # read file docs/mcp_learning/mcp.prompt
80
+ prompt_file_path = os.path.join(os.path.dirname(__file__), "../../docs/mcp_learning/mcp.prompt")
81
+ if os.path.exists(prompt_file_path):
82
+ with open(prompt_file_path, "r") as prompt_file:
83
+ training_prompt = prompt_file.read()
84
+ # print(f"\nLoaded training prompt from {prompt_file_path}:\n{training_prompt}")
85
+ else:
86
+ training_prompt = ""
87
+ print(f"Prompt file not found at {prompt_file_path}.")
88
+
89
+ # if 1 argument, use it as the query
90
+ query_actual = query
91
+ if len(sys.argv) > 1:
92
+ query_actual = sys.argv[1]
93
+ return query_actual + ";\n\n" + training_prompt
94
+
95
+
96
+ def query_llm_with_nl(schema_text, nl_query):
97
+ """
98
+ Query the LLM with a natural language query and schema text to generate a tool context block.
99
+
100
+ It handles both orchestration and simple GET requests.
101
+ """
102
+
103
+ global test_type, create_tool_context_from_llm
104
+
105
+ content = f"Natural language query:\n {nl_query}\nSchema:\n{schema_text}"
106
+ messages = [
107
+ {
108
+ "role": "system",
109
+ "content": "You are an API planner that converts natural language queries into MCP Tool Context blocks using JSON:API. Return only the tool context as JSON."
110
+ },
111
+ {
112
+ "role": "user",
113
+ "content": f"{content}"
114
+ }
115
+ ]
116
+
117
+ request_print = content[0:1200] + '\n... etc' # limit for readability
118
+ print("\n\n2a. LLM request:\n", request_print)
119
+ # print("\n2b. NL Query:\n", nl_query)
120
+ # print("\n2c. schema_text: (truncated) \n")
121
+ # schema_print = json.dumps(json.loads(schema_text), indent=4)[:400] # limit for readability
122
+ # print(schema_print)
123
+
124
+ if create_tool_context_from_llm: # takes 2-3 seconds...
125
+ response = openai.chat.completions.create(
126
+ model="gpt-4",
127
+ messages=messages,
128
+ temperature=0.2
129
+ )
130
+
131
+ tool_context_str = response.choices[0].message.content
132
+ tool_context_str_no_cr = tool_context_str.replace("\n", '') # convert single quotes to double quotes
133
+ try:
134
+ tool_context = json.loads(tool_context_str_no_cr)
135
+ except json.JSONDecodeError:
136
+ print("Failed to decode JSON from response:", tool_context_str)
137
+ return None
138
+
139
+ print("\n2d. generated tool context from LLM:\n", json.dumps(tool_context, indent=4))
140
+
141
+ if "resources" not in tool_context:
142
+ raise ConstraintException("GenAI Error - LLM response does not contain 'resources'.")
143
+ return tool_context
144
+
145
+
146
+ def process_tool_context(tool_context):
147
+ """ Process the orchestration request by executing multiple tool context blocks.
148
+ This executes the tool context blocks against a live JSON:API server.
149
+ It handles both GET and POST requests, and it can
150
+ orchestrate multiple requests based on the provided tool context.
151
+
152
+ Note the orchestration is processed by the client executor (here), not the server executor.
153
+
154
+ Research:
155
+
156
+ 1. How is this a "USB", since the request was specific about JSON:API?
157
+ 2. How is it clear to loop through the tool_context[0] and call tool_context[1]?
158
+ """
159
+ global server_url
160
+
161
+ def get_query_param_filter(query_params):
162
+ """ return json:api filter
163
+
164
+ eg
165
+ curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null},{"name":"CreatedOn","op":"lt","val":"2023-07-14"}]'
166
+
167
+ curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"gt","val":"2023-07-14"}]'
168
+ curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null}]'
169
+ curl -qg 'http://localhost:5656/api/Customer?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]'
170
+
171
+ query_params might be simple:
172
+ "query_params": [ {"name": "credit_limit", "op": "gt", "val": "1000"} ]
173
+ ==> ?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]
174
+
175
+ or a list:
176
+ "query_params": [
177
+ {
178
+ "name": "date_shipped",
179
+ "op": "eq",
180
+ "val": None
181
+ },
182
+ {
183
+ "name": "date_created",
184
+ "op": "lt",
185
+ "val": "2023-07-14"
186
+ }
187
+ ],
188
+
189
+ """
190
+
191
+ added_rows = 0
192
+
193
+ query_param_filter = ''
194
+ assert isinstance(query_params, list), "Query Params filter expected to be a list"
195
+ query_param_filter = 'filter=' + str(query_params)
196
+ # use urlencode to convert to JSON:API format...
197
+ # val urllib.parse.quote() or urllib.parse.urlencode()
198
+ # tool instructions... filtering, email etc "null"
199
+ query_param_filter = query_param_filter.replace("'", '"') # convert single quotes to double quotes
200
+ query_param_filter = query_param_filter.replace("None", 'null')
201
+ query_param_filter = query_param_filter.replace('"null"', 'null')
202
+ # query_param_filter = query_param_filter.replace("date_created", 'CreatedOn') # TODO - why this name?
203
+ return query_param_filter # end get_query_param_filter
204
+
205
+ def move_fields(src: dict, dest: dict, context_data: dict):
206
+ """ Move fields from src to dest, replacing any variables with their values from context_data."""
207
+ for variable_name, value in src.items():
208
+ move_value = value
209
+ if move_value.startswith("{") and move_value.endswith("}"):
210
+ # strip the braces, and get the name after the first dot, # eg: "{Order.customer_id}" ==> "customer_id"``
211
+ move_name = move_value[1:-1] # strip the braces
212
+ if '.' in move_value:
213
+ move_name = move_name.split('.', 1)[1]
214
+ move_value = context_data['attributes'][move_name]
215
+ dest[variable_name] = move_value
216
+ return dest
217
+
218
+ def print_get_response(query_param_filter, mcp_response):
219
+ """ Print the response from the GET request. """
220
+ print("\n3. MCP Server (als) GET filter(query_param_filter):\n", query_param_filter)
221
+ print(" GET Response:\n", mcp_response.text)
222
+ results : List[Dict] = mcp_response.json()['data']
223
+ # print results in a table format
224
+ if results:
225
+ # Get all unique keys from all result dicts
226
+ keys = set()
227
+ for row in results:
228
+ if isinstance(row, dict):
229
+ keys.update(row.keys())
230
+ keys = list(keys)
231
+ # Print header
232
+ print("\n| " + " | ".join(keys) + " |")
233
+ print("|" + "|".join(["---"] * len(keys)) + "|")
234
+ # Print rows
235
+ for row in results:
236
+ print("| " + " | ".join(str(row.get(k, "")) for k in keys) + " |")
237
+ else:
238
+ print("No results found.")
239
+
240
+ assert isinstance(tool_context, (dict, list)), "Tool context expected to be a dictionary"
241
+ context_data = {}
242
+ added_rows = 0
243
+
244
+ for each_block in tool_context["resources"]:
245
+ if process_tool_context := True:
246
+ if each_block["method"] == "GET":
247
+ query_param_filter = get_query_param_filter(each_block["query_params"])
248
+ headers = {"Content-Type": "application/vnd.api+json"}
249
+ if "headers" in each_block:
250
+ headers.update(each_block["headers"])
251
+ mcp_response = requests.get(
252
+ url = each_block["base_url"] + each_block["path"],
253
+ headers=headers,
254
+ params=query_param_filter
255
+ )
256
+ context_data = mcp_response.json()['data'] # result rows...
257
+ print_get_response(query_param_filter, mcp_response)
258
+ elif each_block["method"] in ["POST"]:
259
+ for each_order in context_data:
260
+ url = each_block["base_url"] + each_block["path"]
261
+ json_update_data = { 'data': {"type": "Email", 'attributes': {} } }
262
+ json_update_data_attributes = json_update_data["data"]["attributes"]
263
+ move_fields( src= each_block["body"], dest=json_update_data_attributes, context_data=each_order)
264
+ # eg: POST http://localhost:5656/api/Email {'data': {'type': 'Email', 'attributes': {'customer_id': 5, 'message': {'to': '{{ order.customer_id }}', 'subject': 'Discount for your order', 'body': 'Dear customer, you have a discount for your recent order. Thank you for shopping with us.'}}}}
265
+ headers = {"Content-Type": "application/vnd.api+json"}
266
+ if "headers" in each_block:
267
+ headers.update(each_block["headers"])
268
+ mcp_response = requests.post(
269
+ url=url,
270
+ headers=headers,
271
+ json=json_update_data
272
+ )
273
+ added_rows += 1
274
+ pass
275
+ print("\n3. MCP Server (als) POST Response:\n", mcp_response.text)
276
+ if added_rows > 0:
277
+ print(f"...Added {added_rows} rows to the database; last row (only) shown above.")
278
+ return mcp_response
279
+
280
+
281
+ if __name__ == "__main__":
282
+
283
+ # to run: Run Config > Run designated Python file
284
+
285
+ schema_text = discover_mcp_servers() # see: 1-discovery-from-als
286
+
287
+ query = "List the orders date_shipped is null and CreatedOn before 2023-07-14, and send a discount email (subject: 'Discount Offer') to the customer for each one."
288
+ prompt = get_user_nl_query_and_training(query) # set breakpoint here, view log, then step
289
+
290
+ tool_context = query_llm_with_nl(schema_text, prompt) # see: 2-tool-context-from-LLM
291
+
292
+ mcp_response = process_tool_context(tool_context) # see: 3-MCP-server response
293
+
294
+ print("\nTest complete.\n")
@@ -0,0 +1,25 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "resources": [
4
+ {
5
+ "tool_type": "json-api",
6
+ "base_url": "http://localhost:5656/api",
7
+ "path": "/Order",
8
+ "method": "GET",
9
+ "body": "",
10
+ "query_params": [
11
+ {
12
+ "query_param": "filter=[{\"name\":\"date_shipped\",\"op\":\"lt\",\"val\":\"2023-07-14\"},{\"name\":\"status\",\"op\":\"eq\",\"val\":\"unshipped\"}]"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "tool_type": "json-api",
18
+ "base_url": "http://localhost:5656/api",
19
+ "path": "/Email",
20
+ "method": "POST",
21
+ "body": "{\"subject\": \"Discount Offer\", \"message\": \"You have a new discount offer!\", \"customer_id\": \"{{customer_id}}\"}",
22
+ "query_params": []
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,37 @@
1
+ LLM request - new...?
2
+
3
+ [
4
+ {
5
+ "role": "system",
6
+ "content": "You are an API planner that converts natural language queries into MCP Tool Context blocks using JSON:API. Return only the tool context as JSON."
7
+ },
8
+ {
9
+ "role": "user",
10
+ "content": "Schema:\n{\"base_url\": \"http://localhost:5656/api\", \"description\": \"API Logic Project: basic_demo\", \"email_services\": \"iff email is requested, Send email by issing a POST request to the Email endpoint, setting the subject, message and customer_id in the body.\", \"expected_response\": \"Respond with a JSON object with schema_version and a resource array including: tool_type, base_url, path, method, query_params array or body, headers.\", \"query_params\": \"- JSON:API custom filtering using a filter array (e.g., filter=[{\\\"name\\\":\\\"date_shipped\\\",\\\"op\\\":\\\"gt\\\",\\\"val\\\":\\\"2023-07-14\\\"}])\", \"resources\": [{\"fields\": [\"id\", \"name\", \"balance\", \"credit_limit\"], \"filterable\": [\"id\", \"name\", \"balance\", \"credit_limit\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Customer\", \"path\": \"/Customer\"}, {\"fields\": [\"id\", \"order_id\", \"product_id\", \"quantity\", \"amount\", \"unit_price\"], \"filterable\": [\"id\", \"order_id\", \"product_id\", \"quantity\", \"amount\", \"unit_price\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Item\", \"path\": \"/Item\"}, {\"fields\": [\"id\", \"notes\", \"customer_id\", \"date_shipped\", \"amount_total\"], \"filterable\": [\"id\", \"notes\", \"customer_id\", \"date_shipped\", \"amount_total\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Order\", \"path\": \"/Order\"}, {\"fields\": [\"id\", \"name\", \"unit_price\"], \"filterable\": [\"id\", \"name\", \"unit_price\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Product\", \"path\": \"/Product\"}, {\"fields\": [\"id\", \"request\", \"request_prompt\", \"completion\"], \"filterable\": [\"id\", \"request\", \"request_prompt\", \"completion\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Mcp\", \"path\": \"/Mcp\"}], \"schema_version\": \"1.0\", \"tool_type\": \"json-api\"}\n\nNatural language query: 'List the unshipped orders created before 2023-07-14, and send a discount email (subject: 'Discount Offer') to the customer for each one.'"
11
+ }
12
+ ]
13
+
14
+ LLM request - old
15
+ [
16
+ {
17
+ "role": "system",
18
+ "content": "You are an API planner that converts natural language queries into MCP Tool Context blocks using JSON:API. Return only the tool context as JSON."
19
+ },
20
+ {
21
+ "role": "user",
22
+ "content": "Schema:\n{\"base_url\": \"http://localhost:5656/api\", \"description\": \"API Logic Project: basic_demo\", \"email_services\": \"iff email is requested, Send email by issing a POST request to the Email endpoint, setting the subject, message and customer_id in the body.\", \"expected_response\": \"Respond with a JSON object with schema_version and a resource array including: tool_type, base_url, path, method, query_params array or body, headers.\", \"query_params\": \"- JSON:API custom filtering using a filter array (e.g., filter=[{\\\"name\\\":\\\"date_shipped\\\",\\\"op\\\":\\\"gt\\\",\\\"val\\\":\\\"2023-07-14\\\"}])\", \"resources\": [{\"fields\": [\"id\", \"name\", \"balance\", \"credit_limit\"], \"filterable\": [\"id\", \"name\", \"balance\", \"credit_limit\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Customer\", \"path\": \"/Customer\"}, {\"fields\": [\"id\", \"order_id\", \"product_id\", \"quantity\", \"amount\", \"unit_price\"], \"filterable\": [\"id\", \"order_id\", \"product_id\", \"quantity\", \"amount\", \"unit_price\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Item\", \"path\": \"/Item\"}, {\"fields\": [\"id\", \"notes\", \"customer_id\", \"date_shipped\", \"amount_total\"], \"filterable\": [\"id\", \"notes\", \"customer_id\", \"date_shipped\", \"amount_total\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Order\", \"path\": \"/Order\"}, {\"fields\": [\"id\", \"name\", \"unit_price\"], \"filterable\": [\"id\", \"name\", \"unit_price\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Product\", \"path\": \"/Product\"}, {\"fields\": [\"id\", \"request\", \"request_prompt\", \"completion\"], \"filterable\": [\"id\", \"request\", \"request_prompt\", \"completion\"], \"methods\": [\"GET\", \"PATCH\", \"POST\", \"DELETE\"], \"name\": \"Mcp\", \"path\": \"/Mcp\"}], \"schema_version\": \"1.0\", \"tool_type\": \"json-api\"}\n\nNatural language query: 'List the unshipped orders created before 2023-07-14, and send a discount email (subject: 'Discount Offer') to the customer for each one.'"
23
+ }
24
+ ]
25
+
26
+
27
+ messages - new
28
+ [{'role': 'system', 'content': 'You are an API planner that converts natural language queries into MCP Tool Context b...API. Return only the tool context as JSON.'}, {'role': 'user', 'content': 'Schema:\n{"base_url": "http://localhost:5656/api", "description": "API Logic Project: ...) to the customer for each one.'"}]
29
+
30
+ messages- old
31
+ [{'role': 'system', 'content': 'You are an API planner that converts natural language queries into MCP Tool Context blocks using JSON:API. Return only the tool context as JSON.'}, {'role': 'user', 'content': 'Schema:\n{"base_url": "http://localhost:5656/api", "description": "API Logic Project: basic_demo", "email_services": "iff email is requested, Send email by issing a POST request to the Email endpoint, setting the subject, message and customer_id in the body.", "expected_response": "Respond with a JSON object with schema_version and a resource array including: tool_type, base_url, path, method, query_params array or body, headers.", "query_params": "- JSON:API custom filtering using a filter array (e.g., filter=[{\\"name\\":\\"date_shipped\\",\\"op\\":\\"gt\\",\\"val\\":\\"2023-07-14\\"}])", "resources": [{"fields": ["id", "name", "balance", "credit_limit"], "filterable": ["id", "name", "balance", "credit_limit"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Customer", "path": "/Customer"}, {"fields": ["id", "order_id", "product_id", "quantity", "amount", "unit_price"], "filterable": ["id", "order_id", "product_id", "quantity", "amount", "unit_price"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Item", "path": "/Item"}, {"fields": ["id", "notes", "customer_id", "date_shipped", "amount_total"], "filterable": ["id", "notes", "customer_id", "date_shipped", "amount_total"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Order", "path": "/Order"}, {"fields": ["id", "name", "unit_price"], "filterable": ["id", "name", "unit_price"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Product", "path": "/Product"}, {"fields": ["id", "request", "request_prompt", "completion"], "filterable": ["id", "request", "request_prompt", "completion"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Mcp", "path": "/Mcp"}], "schema_version": "1.0", "tool_type": "json-api"}\n\nNatural language query: \'List the unshipped orders created before 2023-07-14, and send a discount email (subject: \'Discount Offer\') to the customer for each one.\''}]
32
+
33
+ old content
34
+ 'Schema:\n{"base_url": "http://localhost:5656/api", "description": "API Logic Project: basic_demo", "email_services": "iff email is requested, Send email by issing a POST request to the Email endpoint, setting the subject, message and customer_id in the body.", "expected_response": "Respond with a JSON object with schema_version and a resource array including: tool_type, base_url, path, method, query_params array or body, headers.", "query_params": "- JSON:API custom filtering using a filter array (e.g., filter=[{\\"name\\":\\"date_shipped\\",\\"op\\":\\"gt\\",\\"val\\":\\"2023-07-14\\"}])", "resources": [{"fields": ["id", "name", "balance", "credit_limit"], "filterable": ["id", "name", "balance", "credit_limit"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Customer", "path": "/Customer"}, {"fields": ["id", "order_id", "product_id", "quantity", "amount", "unit_price"], "filterable": ["id", "order_id", "product_id", "quantity", "amount", "unit_price"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Item", "path": "/Item"}, {"fields": ["id", "notes", "customer_id", "date_shipped", "amount_total"], "filterable": ["id", "notes", "customer_id", "date_shipped", "amount_total"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Order", "path": "/Order"}, {"fields": ["id", "name", "unit_price"], "filterable": ["id", "name", "unit_price"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Product", "path": "/Product"}, {"fields": ["id", "request", "request_prompt", "completion"], "filterable": ["id", "request", "request_prompt", "completion"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Mcp", "path": "/Mcp"}], "schema_version": "1.0", "tool_type": "json-api"}\n\nNatural language query: \'List the unshipped orders created before 2023-07-14, and send a discount email (subject: \'Discount Offer\') to the customer for each one.\''
35
+
36
+ where new content looks correct:
37
+ 'Schema:\n{"base_url": "http://localhost:5656/api", "description": "API Logic Project: basic_demo", "email_services": "iff email is requested, Send email by issing a POST request to the Email endpoint, setting the subject, message and customer_id in the body.", "expected_response": "Respond with a JSON object with schema_version and a resource array including: tool_type, base_url, path, method, query_params array or body, headers.", "query_params": "- JSON:API custom filtering using a filter array (e.g., filter=[{\\"name\\":\\"date_shipped\\",\\"op\\":\\"gt\\",\\"val\\":\\"2023-07-14\\"}])", "resources": [{"fields": ["id", "name", "balance", "credit_limit"], "filterable": ["id", "name", "balance", "credit_limit"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Customer", "path": "/Customer"}, {"fields": ["id", "order_id", "product_id", "quantity", "amount", "unit_price"], "filterable": ["id", "order_id", "product_id", "quantity", "amount", "unit_price"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Item", "path": "/Item"}, {"fields": ["id", "notes", "customer_id", "date_shipped", "amount_total"], "filterable": ["id", "notes", "customer_id", "date_shipped", "amount_total"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Order", "path": "/Order"}, {"fields": ["id", "name", "unit_price"], "filterable": ["id", "name", "unit_price"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Product", "path": "/Product"}, {"fields": ["id", "request", "request_prompt", "completion"], "filterable": ["id", "request", "request_prompt", "completion"], "methods": ["GET", "PATCH", "POST", "DELETE"], "name": "Mcp", "path": "/Mcp"}], "schema_version": "1.0", "tool_type": "json-api"}\n\nNatural language query: \'List the unshipped orders created before 2023-07-14, and send a discount email (subject: \'Discount Offer\') to the customer for each one.\''
@@ -33,7 +33,7 @@ def declare_logic():
33
33
  discover_logic()
34
34
 
35
35
  # Logic from GenAI: (or, use your IDE w/ code completion)
36
- from database.models import Product, Order, Item, Customer, Email
36
+ from database.models import Product, Order, Item, Customer, SysEmail
37
37
 
38
38
  # Ensure the customer's balance is less than their credit limit
39
39
  Rule.constraint(validate=Customer, as_condition=lambda row: row.balance <= row.credit_limit, error_msg="Customer balance ({row.balance}) exceeds credit limit ({row.credit_limit})")
@@ -54,25 +54,6 @@ def declare_logic():
54
54
  Rule.after_flush_row_event(on_class=Order, calling=kafka_producer.send_row_to_kafka, if_condition=lambda row: row.date_shipped is not None, with_args={'topic': 'order_shipping'})
55
55
 
56
56
  # End Logic from GenAI
57
-
58
- def send_mail(row: Email, old_row: Email, logic_row: LogicRow):
59
- """
60
-
61
- #als: Send N8N email message (also see discovery/integration.py)
62
-
63
- Args:
64
- row (Email): inserted Email
65
- old_row (Email): n/a
66
- logic_row (LogicRow): bundles curr/old row, with ins/upd/dlt logic
67
- """
68
- if logic_row.is_inserted():
69
- customer = row.customer # parent accessor
70
- if customer.email_opt_out:
71
- logic_row.log("customer opted out of email")
72
- return
73
- logic_row.log(f"send email {row.message} to {customer.email} (stub, eg use N8N") # see in log
74
-
75
- Rule.after_flush_row_event(on_class=Email, calling=send_mail) # see above
76
57
 
77
58
 
78
59
  def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
@@ -0,0 +1,47 @@
1
+ import datetime
2
+ from decimal import Decimal
3
+ from logic_bank.exec_row_logic.logic_row import LogicRow
4
+ from logic_bank.extensions.rule_extensions import RuleExtension
5
+ from logic_bank.logic_bank import Rule
6
+ from database import models
7
+ import api.system.opt_locking.opt_locking as opt_locking
8
+ from security.system.authorization import Grant
9
+ import logging
10
+ from flask import jsonify
11
+
12
+ app_logger = logging.getLogger(__name__)
13
+
14
+ def declare_logic():
15
+ """
16
+ This illustrates the request pattern.
17
+
18
+ The request pattern is a common pattern in API Logic Server,
19
+ where an insert triggers service invocation, such as sending email.
20
+
21
+ The Email table includes the columns for the email (e,g, recipient, subject, message).
22
+
23
+ Using a request object enables you to wrap the service call with logic, eg:
24
+
25
+ * *email requirement: do not send mail if customer has opted out*
26
+
27
+ See: https://apilogicserver.github.io/Docs/Integration-MCP/#3a-logic-request-pattern
28
+ """
29
+
30
+ def send_mail(row: models.SysEmail, old_row: models.SysEmail, logic_row: LogicRow):
31
+ """
32
+
33
+ #als: Send N8N email message
34
+
35
+ Args:
36
+ row (Email): inserted Email
37
+ old_row (Email): n/a
38
+ logic_row (LogicRow): bundles curr/old row, with ins/upd/dlt logic
39
+ """
40
+ if logic_row.is_inserted():
41
+ customer = row.customer # parent accessor
42
+ if customer.email_opt_out:
43
+ logic_row.log("customer opted out of email")
44
+ return
45
+ logic_row.log(f"send email {row.message} to {customer.email} (stub, eg use N8N") # see in log
46
+
47
+ Rule.after_flush_row_event(on_class=models.SysEmail, calling=send_mail) # see above