ApiLogicServer 14.5.4__py3-none-any.whl → 14.5.15__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/api_logic_server.py +4 -122
- api_logic_server_cli/api_logic_server_info.yaml +3 -3
- api_logic_server_cli/create_from_model/__pycache__/api_logic_server_utils.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/api_logic_server_utils.py +29 -10
- api_logic_server_cli/create_from_model/dbml.py +15 -12
- api_logic_server_cli/database/basic_demo.sqlite +0 -0
- api_logic_server_cli/genai/genai.py +4 -0
- api_logic_server_cli/genai/genai_svcs.py +14 -3
- api_logic_server_cli/manager.py +3 -1
- api_logic_server_cli/prototypes/base/.vscode/launch.json +40 -0
- api_logic_server_cli/prototypes/base/api/api_discovery/mcp_discovery.py +58 -0
- api_logic_server_cli/prototypes/base/database/system/SAFRSBaseX.py +68 -2
- api_logic_server_cli/prototypes/base/database/system/SAFRSBaseX.pyZ +73 -0
- api_logic_server_cli/prototypes/{basic_demo/customizations/integration → base/integration/mcp}/.DS_Store +0 -0
- api_logic_server_cli/prototypes/base/integration/mcp/README_mcp.md +15 -0
- api_logic_server_cli/prototypes/{basic_demo/customizations → base}/integration/mcp/mcp_client_executor.py +91 -103
- api_logic_server_cli/prototypes/base/integration/mcp/mcp_schema.txt +47 -0
- api_logic_server_cli/prototypes/base/integration/mcp/mcp_server_discovery.json +9 -0
- api_logic_server_cli/prototypes/base/integration/mcp/test_notes.txt +37 -0
- api_logic_server_cli/prototypes/basic_demo/README.md +251 -91
- api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/mcp_discovery.py +1 -1
- api_logic_server_cli/prototypes/basic_demo/customizations/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/database/models.py +27 -12
- api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +1 -1
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/Zmcp_client_executor.py +294 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_tool_context.json +25 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/test_notes.txt +37 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +1 -20
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/email_request.py +47 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/mcp_client_executor_request.py +320 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/simple_constraints.py +25 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/admin.yaml +39 -32
- api_logic_server_cli/prototypes/basic_demo/iteration/database/db.sqlite +0 -0
- api_logic_server_cli/prototypes/basic_demo/iteration/ui/admin/admin.yaml +39 -44
- api_logic_server_cli/prototypes/manager/.vscode/launch.json +21 -0
- api_logic_server_cli/prototypes/manager/{README.md → READMEz.md} +4 -4
- api_logic_server_cli/prototypes/manager/REAMDE.md +1057 -0
- api_logic_server_cli/prototypes/manager/system/genai/mcp_learning/mcp.prompt +27 -0
- api_logic_server_cli/prototypes/manager/system/install-ApiLogicServer-dev/install-ApiLogicServer-dev.sh +2 -1
- {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.15.dist-info}/METADATA +1 -1
- {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.15.dist-info}/RECORD +46 -42
- {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.15.dist-info}/WHEEL +1 -1
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/3_executor_test_agent.py +0 -52
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/README_functon.md +0 -201
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/ai_plugin.json +0 -17
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/nw-swagger_3.json +0 -1731
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/snippets.txt +0 -5
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3 genai_demo_with_get.json +0 -1731
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3.json +0 -1782
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo.json +0 -264
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/openai_function/swagger_3_genai_demo_with_update.json +0 -1782
- api_logic_server_cli/prototypes/genai_demo/logic/logic_discovery/auto_discovery.py +0 -52
- {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.15.dist-info}/entry_points.txt +0 -0
- {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.15.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-14.5.4.dist-info → apilogicserver-14.5.15.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
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.exec_row_logic.logic_row import LogicRow
|
|
21
|
+
from logic_bank.logic_bank import Rule
|
|
22
|
+
from database import models
|
|
23
|
+
from logic_bank.util import ConstraintException
|
|
24
|
+
|
|
25
|
+
# Set your OpenAI API key
|
|
26
|
+
openai.api_key = os.getenv("APILOGICSERVER_CHATGPT_APIKEY")
|
|
27
|
+
|
|
28
|
+
server_url = os.getenv("APILOGICSERVER_URL", "http://localhost:5656/api")
|
|
29
|
+
|
|
30
|
+
# debug settings
|
|
31
|
+
test_type = 'orchestration' # 'simple_get' or 'orchestration'
|
|
32
|
+
create_tool_context_from_llm = True
|
|
33
|
+
''' set to False to bypass LLM call and save 2-3 secs in testing '''
|
|
34
|
+
use_test_schema = False
|
|
35
|
+
''' True means bypass discovery, use hard-coded schedma file '''
|
|
36
|
+
|
|
37
|
+
def discover_mcp_servers():
|
|
38
|
+
""" Discover the MCP servers by calling the /api/.well-known/mcp.json endpoint.
|
|
39
|
+
This function retrieves the list of available MCP servers and their capabilities.
|
|
40
|
+
"""
|
|
41
|
+
global server_url, use_test_schema
|
|
42
|
+
|
|
43
|
+
# create schema_text (for prompt), by reading integration/mcp/mcp_schema.txt
|
|
44
|
+
|
|
45
|
+
# find the servers - read the mcp_server_discovery.json file
|
|
46
|
+
discovery_file_path = os.path.join(os.path.dirname(__file__), "../../integration/mcp/mcp_server_discovery.json")
|
|
47
|
+
try:
|
|
48
|
+
with open(discovery_file_path, "r") as discovery_file:
|
|
49
|
+
discovery_data = json.load(discovery_file)
|
|
50
|
+
print(f"\n1. Discovered MCP servers from config file: {discovery_file_path}:" + json.dumps(discovery_data, indent=4))
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
print(f"Discovery file not found at {discovery_file_path}.")
|
|
53
|
+
except json.JSONDecodeError as e:
|
|
54
|
+
print(f"Error decoding JSON from {discovery_file_path}: {e}")
|
|
55
|
+
|
|
56
|
+
for each_server in discovery_data["servers"]:
|
|
57
|
+
discovery_url = each_server["schema_url"]
|
|
58
|
+
|
|
59
|
+
# Call the discovery_url to get the MCP/API schema
|
|
60
|
+
try:
|
|
61
|
+
response = requests.get(discovery_url)
|
|
62
|
+
if response.status_code == 200:
|
|
63
|
+
api_schema = response.json()
|
|
64
|
+
print()
|
|
65
|
+
request_print = json.dumps(api_schema, indent=4)[0:400] + '\n... etc' # limit for readability
|
|
66
|
+
print(f"\n\nAPI Schema from discovery schema_url: {discovery_url}:\n" + request_print)
|
|
67
|
+
else:
|
|
68
|
+
print(f"Failed to retrieve API schema from {discovery_url}: {response.status_code}")
|
|
69
|
+
except requests.RequestException as e:
|
|
70
|
+
print(f"Error calling OpenAPI URL: {e}")
|
|
71
|
+
return json.dumps(api_schema)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_user_nl_query_and_training(query: str):
|
|
75
|
+
""" Get the natural language query from the user.
|
|
76
|
+
Add training for the LLM to generate a tool context block.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
global test_type
|
|
81
|
+
# read file docs/mcp_learning/mcp.prompt
|
|
82
|
+
prompt_file_path = os.path.join(os.path.dirname(__file__), "../../docs/mcp_learning/mcp.prompt")
|
|
83
|
+
if os.path.exists(prompt_file_path):
|
|
84
|
+
with open(prompt_file_path, "r") as prompt_file:
|
|
85
|
+
training_prompt = prompt_file.read()
|
|
86
|
+
# print(f"\nLoaded training prompt from {prompt_file_path}:\n{training_prompt}")
|
|
87
|
+
else:
|
|
88
|
+
training_prompt = ""
|
|
89
|
+
print(f"Prompt file not found at {prompt_file_path}.")
|
|
90
|
+
return query + "\n\n" + training_prompt
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def query_llm_with_nl(schema_text, nl_query):
|
|
94
|
+
"""
|
|
95
|
+
Query the LLM with a natural language query and schema text to generate a tool context block.
|
|
96
|
+
|
|
97
|
+
It handles both orchestration and simple GET requests.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
global test_type, create_tool_context_from_llm
|
|
101
|
+
|
|
102
|
+
content = f"Natural language query:\n {nl_query}\nSchema:\n{schema_text}"
|
|
103
|
+
messages = [
|
|
104
|
+
{
|
|
105
|
+
"role": "system",
|
|
106
|
+
"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."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"role": "user",
|
|
110
|
+
"content": f"{content}"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
request_print = content[0:1200] + '\n... etc' # limit for readability
|
|
115
|
+
print("\n\n2a. LLM request:\n", request_print)
|
|
116
|
+
# print("\n2b. NL Query:\n", nl_query)
|
|
117
|
+
# print("\n2c. schema_text: (truncated) \n")
|
|
118
|
+
# schema_print = json.dumps(json.loads(schema_text), indent=4)[:400] # limit for readability
|
|
119
|
+
# print(schema_print)
|
|
120
|
+
|
|
121
|
+
if create_tool_context_from_llm: # takes 2-3 seconds...
|
|
122
|
+
response = openai.chat.completions.create(
|
|
123
|
+
model="gpt-4",
|
|
124
|
+
messages=messages,
|
|
125
|
+
temperature=0.2
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
tool_context_str = response.choices[0].message.content
|
|
129
|
+
tool_context_str_no_cr = tool_context_str.replace("\n", '') # convert single quotes to double quotes
|
|
130
|
+
try:
|
|
131
|
+
tool_context = json.loads(tool_context_str_no_cr)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
print("Failed to decode JSON from response:", tool_context_str)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
print("\n2d. generated tool context from LLM:\n", json.dumps(tool_context, indent=4))
|
|
137
|
+
|
|
138
|
+
if "resources" not in tool_context:
|
|
139
|
+
raise ConstraintException("GenAI Error - LLM response does not contain 'resources'.")
|
|
140
|
+
return tool_context
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def process_tool_context(tool_context):
|
|
144
|
+
""" Process the orchestration request by executing multiple tool context blocks.
|
|
145
|
+
This executes the tool context blocks against a live JSON:API server.
|
|
146
|
+
It handles both GET and POST requests, and it can
|
|
147
|
+
orchestrate multiple requests based on the provided tool context.
|
|
148
|
+
|
|
149
|
+
Note the orchestration is processed by the client executor (here), not the server executor.
|
|
150
|
+
|
|
151
|
+
Research:
|
|
152
|
+
|
|
153
|
+
1. How is this a "USB", since the request was specific about JSON:API?
|
|
154
|
+
2. How is it clear to loop through the tool_context[0] and call tool_context[1]?
|
|
155
|
+
"""
|
|
156
|
+
global server_url
|
|
157
|
+
|
|
158
|
+
def get_query_param_filter(query_params):
|
|
159
|
+
""" return json:api filter
|
|
160
|
+
|
|
161
|
+
eg
|
|
162
|
+
curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null},{"name":"CreatedOn","op":"lt","val":"2023-07-14"}]'
|
|
163
|
+
|
|
164
|
+
curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"gt","val":"2023-07-14"}]'
|
|
165
|
+
curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null}]'
|
|
166
|
+
curl -qg 'http://localhost:5656/api/Customer?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]'
|
|
167
|
+
|
|
168
|
+
query_params might be simple:
|
|
169
|
+
"query_params": [ {"name": "credit_limit", "op": "gt", "val": "1000"} ]
|
|
170
|
+
==> ?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]
|
|
171
|
+
|
|
172
|
+
or a list:
|
|
173
|
+
"query_params": [
|
|
174
|
+
{
|
|
175
|
+
"name": "date_shipped",
|
|
176
|
+
"op": "eq",
|
|
177
|
+
"val": None
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "date_created",
|
|
181
|
+
"op": "lt",
|
|
182
|
+
"val": "2023-07-14"
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
added_rows = 0
|
|
189
|
+
|
|
190
|
+
query_param_filter = ''
|
|
191
|
+
assert isinstance(query_params, list), "Query Params filter expected to be a list"
|
|
192
|
+
query_param_filter = 'filter=' + str(query_params)
|
|
193
|
+
# use urlencode to convert to JSON:API format...
|
|
194
|
+
# val urllib.parse.quote() or urllib.parse.urlencode()
|
|
195
|
+
# tool instructions... filtering, email etc "null"
|
|
196
|
+
query_param_filter = query_param_filter.replace("'", '"') # convert single quotes to double quotes
|
|
197
|
+
query_param_filter = query_param_filter.replace("None", 'null')
|
|
198
|
+
query_param_filter = query_param_filter.replace('"null"', 'null')
|
|
199
|
+
# query_param_filter = query_param_filter.replace("date_created", 'CreatedOn') # TODO - why this name?
|
|
200
|
+
return query_param_filter # end get_query_param_filter
|
|
201
|
+
|
|
202
|
+
def move_fields(src: dict, dest: dict, context_data: dict):
|
|
203
|
+
""" Move fields from src to dest, replacing any variables with their values from context_data."""
|
|
204
|
+
for variable_name, value in src.items():
|
|
205
|
+
move_value = value
|
|
206
|
+
if move_value.startswith("{") and move_value.endswith("}"):
|
|
207
|
+
# strip the braces, and get the name after the first dot, # eg: "{Order.customer_id}" ==> "customer_id"``
|
|
208
|
+
move_name = move_value[1:-1] # strip the braces
|
|
209
|
+
if '.' in move_value:
|
|
210
|
+
move_name = move_name.split('.', 1)[1]
|
|
211
|
+
move_value = context_data['attributes'][move_name]
|
|
212
|
+
dest[variable_name] = move_value
|
|
213
|
+
return dest
|
|
214
|
+
|
|
215
|
+
def print_get_response(query_param_filter, mcp_response):
|
|
216
|
+
""" Print the response from the GET request. """
|
|
217
|
+
print("\n3. MCP Server (als) GET filter(query_param_filter):\n", query_param_filter)
|
|
218
|
+
print(" GET Response:\n", mcp_response.text)
|
|
219
|
+
results : List[Dict] = mcp_response.json()['data']
|
|
220
|
+
# print results in a table format
|
|
221
|
+
if results:
|
|
222
|
+
# Get all unique keys from all result dicts
|
|
223
|
+
keys = set()
|
|
224
|
+
for row in results:
|
|
225
|
+
if isinstance(row, dict):
|
|
226
|
+
keys.update(row.keys())
|
|
227
|
+
keys = list(keys)
|
|
228
|
+
# Print header
|
|
229
|
+
print("\n| " + " | ".join(keys) + " |")
|
|
230
|
+
print("|" + "|".join(["---"] * len(keys)) + "|")
|
|
231
|
+
# Print rows
|
|
232
|
+
for row in results:
|
|
233
|
+
print("| " + " | ".join(str(row.get(k, "")) for k in keys) + " |")
|
|
234
|
+
else:
|
|
235
|
+
print("No results found.")
|
|
236
|
+
|
|
237
|
+
assert isinstance(tool_context, (dict, list)), "Tool context expected to be a dictionary"
|
|
238
|
+
context_data = {}
|
|
239
|
+
added_rows = 0
|
|
240
|
+
|
|
241
|
+
for each_block in tool_context["resources"]:
|
|
242
|
+
if process_tool_context := True:
|
|
243
|
+
if each_block["method"] == "GET":
|
|
244
|
+
query_param_filter = get_query_param_filter(each_block["query_params"])
|
|
245
|
+
headers = {"Content-Type": "application/vnd.api+json"}
|
|
246
|
+
if "headers" in each_block:
|
|
247
|
+
headers.update(each_block["headers"])
|
|
248
|
+
mcp_response = requests.get(
|
|
249
|
+
url = each_block["base_url"] + each_block["path"],
|
|
250
|
+
headers=headers,
|
|
251
|
+
params=query_param_filter
|
|
252
|
+
)
|
|
253
|
+
context_data = mcp_response.json()['data'] # result rows...
|
|
254
|
+
print_get_response(query_param_filter, mcp_response)
|
|
255
|
+
elif each_block["method"] in ["POST"]:
|
|
256
|
+
for each_order in context_data:
|
|
257
|
+
url = each_block["base_url"] + each_block["path"]
|
|
258
|
+
json_update_data = { 'data': {"type": each_block["path"][1:], 'attributes': {} } }
|
|
259
|
+
json_update_data_attributes = json_update_data["data"]["attributes"]
|
|
260
|
+
move_fields( src= each_block["body"], dest=json_update_data_attributes, context_data=each_order)
|
|
261
|
+
# eg: POST http://localhost:5656/api/SysEmail {'data': {'type': 'SysEmail', '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.'}}}}
|
|
262
|
+
headers = {"Content-Type": "application/vnd.api+json"}
|
|
263
|
+
if "headers" in each_block:
|
|
264
|
+
headers.update(each_block["headers"])
|
|
265
|
+
mcp_response = requests.post(
|
|
266
|
+
url=url,
|
|
267
|
+
headers=headers,
|
|
268
|
+
json=json_update_data
|
|
269
|
+
)
|
|
270
|
+
added_rows += 1
|
|
271
|
+
pass
|
|
272
|
+
print("\n3. MCP Server (als) POST Response:\n", mcp_response.text)
|
|
273
|
+
if added_rows > 0:
|
|
274
|
+
print(f"...Added {added_rows} rows to the database; last row (only) shown above.")
|
|
275
|
+
return mcp_response
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def declare_logic():
|
|
280
|
+
"""
|
|
281
|
+
This illustrates the request pattern.
|
|
282
|
+
|
|
283
|
+
The request pattern is a common pattern in API Logic Server,
|
|
284
|
+
where an insert triggers service invocation, such as sending email.
|
|
285
|
+
|
|
286
|
+
The Email table includes the columns for the email (e,g, recipient, subject, message).
|
|
287
|
+
|
|
288
|
+
Using a request object enables you to wrap the service call with logic, eg:
|
|
289
|
+
|
|
290
|
+
* *email requirement: do not send mail if customer has opted out*
|
|
291
|
+
|
|
292
|
+
See: https://apilogicserver.github.io/Docs/Integration-MCP/#3a-logic-request-pattern
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def mcp_client_executor(row: models.SysMcp, old_row: models.SysMcp, logic_row: LogicRow):
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
#als: create an MCP request
|
|
300
|
+
|
|
301
|
+
curl -X 'POST' 'http://localhost:5656/api/SysMcp/' -H 'accept: application/vnd.api+json' -H 'Content-Type: application/json' -d '{ "data": { "attributes": {"request": "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."}, "type": "SysMcp"}}'
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
row (Mcp): inserted MCP with prompt
|
|
305
|
+
old_row (Mcp): n/a
|
|
306
|
+
logic_row (LogicRow): bundles curr/old row, with ins/upd/dlt logic
|
|
307
|
+
"""
|
|
308
|
+
schema_text = discover_mcp_servers() # see: 1-discovery-from-als
|
|
309
|
+
|
|
310
|
+
query_example = "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."
|
|
311
|
+
query = row.request
|
|
312
|
+
prompt = get_user_nl_query_and_training(query)
|
|
313
|
+
|
|
314
|
+
tool_context = query_llm_with_nl(schema_text, prompt) # see: 2-tool-context-from-LLM
|
|
315
|
+
|
|
316
|
+
mcp_response = process_tool_context(tool_context) # see: 3-MCP-server response
|
|
317
|
+
|
|
318
|
+
print("\nTest complete.\n")
|
|
319
|
+
|
|
320
|
+
Rule.row_event(on_class=models.SysMcp, calling=mcp_client_executor) # see above
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
Simple constraints for error testing, e.g.:
|
|
17
|
+
|
|
18
|
+
* Set a breakpoint on `as_condition`
|
|
19
|
+
* Observe the row values in the debugger
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
Rule.constraint(validate=models.Customer,
|
|
23
|
+
as_condition=lambda row: row.name != 'x',
|
|
24
|
+
error_msg="Customer name cannot be 'x'")
|
|
25
|
+
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
about:
|
|
2
|
-
date: May
|
|
3
|
-
merged:
|
|
4
|
-
at: May 13, 2025 20:15:21
|
|
5
|
-
new_attributes: 'Customer.email Order.CreatedOn '
|
|
6
|
-
new_resources: 'Email '
|
|
7
|
-
new_tab_groups: 'Customer.EmailList '
|
|
2
|
+
date: May 26, 2025 06:57:17
|
|
8
3
|
recent_changes: works with modified safrs-react-admin
|
|
9
4
|
version: 0.0.0
|
|
10
5
|
api_root: '{http_type}://{swagger_host}:{port}/{api}'
|
|
11
6
|
authentication: '{system-default}'
|
|
12
7
|
info:
|
|
13
8
|
number_relationships: 4
|
|
14
|
-
number_tables:
|
|
9
|
+
number_tables: 6
|
|
15
10
|
info_toggle_checked: true
|
|
16
11
|
resources:
|
|
17
12
|
Customer:
|
|
@@ -24,10 +19,10 @@ resources:
|
|
|
24
19
|
type: DECIMAL
|
|
25
20
|
- name: credit_limit
|
|
26
21
|
type: DECIMAL
|
|
27
|
-
- name: id
|
|
28
22
|
- name: email
|
|
29
23
|
- name: email_opt_out
|
|
30
|
-
type:
|
|
24
|
+
type: Boolean
|
|
25
|
+
- name: id
|
|
31
26
|
tab_groups:
|
|
32
27
|
- direction: tomany
|
|
33
28
|
fks:
|
|
@@ -37,29 +32,10 @@ resources:
|
|
|
37
32
|
- direction: tomany
|
|
38
33
|
fks:
|
|
39
34
|
- customer_id
|
|
40
|
-
name:
|
|
41
|
-
resource:
|
|
35
|
+
name: SysEmailList
|
|
36
|
+
resource: SysEmail
|
|
42
37
|
type: Customer
|
|
43
38
|
user_key: name
|
|
44
|
-
Email:
|
|
45
|
-
attributes:
|
|
46
|
-
- label: ' id*'
|
|
47
|
-
name: id
|
|
48
|
-
search: true
|
|
49
|
-
sort: true
|
|
50
|
-
- name: customer_id
|
|
51
|
-
required: true
|
|
52
|
-
- name: message
|
|
53
|
-
- name: CreatedOn
|
|
54
|
-
type: DATE
|
|
55
|
-
tab_groups:
|
|
56
|
-
- direction: toone
|
|
57
|
-
fks:
|
|
58
|
-
- customer_id
|
|
59
|
-
name: customer
|
|
60
|
-
resource: Customer
|
|
61
|
-
type: Email
|
|
62
|
-
user_key: id
|
|
63
39
|
Item:
|
|
64
40
|
attributes:
|
|
65
41
|
- label: ' id*'
|
|
@@ -97,12 +73,12 @@ resources:
|
|
|
97
73
|
- name: customer_id
|
|
98
74
|
required: true
|
|
99
75
|
- name: notes
|
|
76
|
+
- name: CreatedOn
|
|
77
|
+
type: DATE
|
|
100
78
|
- name: amount_total
|
|
101
79
|
type: DECIMAL
|
|
102
80
|
- name: date_shipped
|
|
103
81
|
type: DATE
|
|
104
|
-
- name: CreatedOn
|
|
105
|
-
type: DATE
|
|
106
82
|
tab_groups:
|
|
107
83
|
- direction: tomany
|
|
108
84
|
fks:
|
|
@@ -133,6 +109,37 @@ resources:
|
|
|
133
109
|
resource: Item
|
|
134
110
|
type: Product
|
|
135
111
|
user_key: name
|
|
112
|
+
SysEmail:
|
|
113
|
+
attributes:
|
|
114
|
+
- label: ' id*'
|
|
115
|
+
name: id
|
|
116
|
+
search: true
|
|
117
|
+
sort: true
|
|
118
|
+
- name: customer_id
|
|
119
|
+
required: true
|
|
120
|
+
- name: message
|
|
121
|
+
- name: subject
|
|
122
|
+
- name: CreatedOn
|
|
123
|
+
type: DATE
|
|
124
|
+
tab_groups:
|
|
125
|
+
- direction: toone
|
|
126
|
+
fks:
|
|
127
|
+
- customer_id
|
|
128
|
+
name: customer
|
|
129
|
+
resource: Customer
|
|
130
|
+
type: SysEmail
|
|
131
|
+
user_key: id
|
|
132
|
+
SysMcp:
|
|
133
|
+
attributes:
|
|
134
|
+
- label: ' id*'
|
|
135
|
+
name: id
|
|
136
|
+
search: true
|
|
137
|
+
sort: true
|
|
138
|
+
- name: request
|
|
139
|
+
- name: request_prompt
|
|
140
|
+
- name: completion
|
|
141
|
+
type: SysMcp
|
|
142
|
+
user_key: id
|
|
136
143
|
settings:
|
|
137
144
|
HomeJS: /admin-app/home.js
|
|
138
145
|
max_list_columns: 8
|
|
Binary file
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
about:
|
|
2
|
-
date: May
|
|
3
|
-
merged:
|
|
4
|
-
at: May 13, 2025 20:15:21
|
|
5
|
-
new_attributes: 'Customer.email Order.CreatedOn '
|
|
6
|
-
new_resources: 'Email '
|
|
7
|
-
new_tab_groups: 'Customer.EmailList '
|
|
2
|
+
date: May 26, 2025 06:57:17
|
|
8
3
|
recent_changes: works with modified safrs-react-admin
|
|
9
4
|
version: 0.0.0
|
|
10
5
|
api_root: '{http_type}://{swagger_host}:{port}/{api}'
|
|
11
6
|
authentication: '{system-default}'
|
|
12
7
|
info:
|
|
13
8
|
number_relationships: 4
|
|
14
|
-
number_tables:
|
|
9
|
+
number_tables: 6
|
|
15
10
|
info_toggle_checked: true
|
|
16
11
|
resources:
|
|
17
12
|
Customer:
|
|
@@ -24,14 +19,10 @@ resources:
|
|
|
24
19
|
type: DECIMAL
|
|
25
20
|
- name: credit_limit
|
|
26
21
|
type: DECIMAL
|
|
27
|
-
- name: id
|
|
28
22
|
- name: email
|
|
29
23
|
- name: email_opt_out
|
|
30
|
-
type:
|
|
31
|
-
|
|
32
|
-
limit.
|
|
33
|
-
info_list: Defines the Customer entity with a unique name, balance, and credit
|
|
34
|
-
limit.
|
|
24
|
+
type: Boolean
|
|
25
|
+
- name: id
|
|
35
26
|
tab_groups:
|
|
36
27
|
- direction: tomany
|
|
37
28
|
fks:
|
|
@@ -41,29 +32,10 @@ resources:
|
|
|
41
32
|
- direction: tomany
|
|
42
33
|
fks:
|
|
43
34
|
- customer_id
|
|
44
|
-
name:
|
|
45
|
-
resource:
|
|
35
|
+
name: SysEmailList
|
|
36
|
+
resource: SysEmail
|
|
46
37
|
type: Customer
|
|
47
38
|
user_key: name
|
|
48
|
-
Email:
|
|
49
|
-
attributes:
|
|
50
|
-
- label: ' id*'
|
|
51
|
-
name: id
|
|
52
|
-
search: true
|
|
53
|
-
sort: true
|
|
54
|
-
- name: customer_id
|
|
55
|
-
required: true
|
|
56
|
-
- name: message
|
|
57
|
-
- name: CreatedOn
|
|
58
|
-
type: DATE
|
|
59
|
-
tab_groups:
|
|
60
|
-
- direction: toone
|
|
61
|
-
fks:
|
|
62
|
-
- customer_id
|
|
63
|
-
name: customer
|
|
64
|
-
resource: Customer
|
|
65
|
-
type: Email
|
|
66
|
-
user_key: id
|
|
67
39
|
Item:
|
|
68
40
|
attributes:
|
|
69
41
|
- label: ' id*'
|
|
@@ -79,8 +51,6 @@ resources:
|
|
|
79
51
|
type: DECIMAL
|
|
80
52
|
- name: unit_price
|
|
81
53
|
type: DECIMAL
|
|
82
|
-
description: Defines the Item entity with quantity, amounts, and unit price details.
|
|
83
|
-
info_list: Defines the Item entity with quantity, amounts, and unit price details.
|
|
84
54
|
tab_groups:
|
|
85
55
|
- direction: toone
|
|
86
56
|
fks:
|
|
@@ -103,16 +73,12 @@ resources:
|
|
|
103
73
|
- name: customer_id
|
|
104
74
|
required: true
|
|
105
75
|
- name: notes
|
|
76
|
+
- name: CreatedOn
|
|
77
|
+
type: DATE
|
|
106
78
|
- name: amount_total
|
|
107
79
|
type: DECIMAL
|
|
108
80
|
- name: date_shipped
|
|
109
81
|
type: DATE
|
|
110
|
-
- name: CreatedOn
|
|
111
|
-
type: DATE
|
|
112
|
-
description: Defines the Order entity which belongs to a customer. Includes notes
|
|
113
|
-
and amount total.
|
|
114
|
-
info_list: Defines the Order entity which belongs to a customer. Includes notes
|
|
115
|
-
and amount total.
|
|
116
82
|
tab_groups:
|
|
117
83
|
- direction: tomany
|
|
118
84
|
fks:
|
|
@@ -137,8 +103,6 @@ resources:
|
|
|
137
103
|
- name: carbon_neutral
|
|
138
104
|
type: BOOLEAN
|
|
139
105
|
- name: id
|
|
140
|
-
description: Defines the Product entity with a unique name and unit price.
|
|
141
|
-
info_list: Defines the Product entity with a unique name and unit price.
|
|
142
106
|
tab_groups:
|
|
143
107
|
- direction: tomany
|
|
144
108
|
fks:
|
|
@@ -147,6 +111,37 @@ resources:
|
|
|
147
111
|
resource: Item
|
|
148
112
|
type: Product
|
|
149
113
|
user_key: name
|
|
114
|
+
SysEmail:
|
|
115
|
+
attributes:
|
|
116
|
+
- label: ' id*'
|
|
117
|
+
name: id
|
|
118
|
+
search: true
|
|
119
|
+
sort: true
|
|
120
|
+
- name: customer_id
|
|
121
|
+
required: true
|
|
122
|
+
- name: message
|
|
123
|
+
- name: subject
|
|
124
|
+
- name: CreatedOn
|
|
125
|
+
type: DATE
|
|
126
|
+
tab_groups:
|
|
127
|
+
- direction: toone
|
|
128
|
+
fks:
|
|
129
|
+
- customer_id
|
|
130
|
+
name: customer
|
|
131
|
+
resource: Customer
|
|
132
|
+
type: SysEmail
|
|
133
|
+
user_key: id
|
|
134
|
+
SysMcp:
|
|
135
|
+
attributes:
|
|
136
|
+
- label: ' id*'
|
|
137
|
+
name: id
|
|
138
|
+
search: true
|
|
139
|
+
sort: true
|
|
140
|
+
- name: request
|
|
141
|
+
- name: request_prompt
|
|
142
|
+
- name: completion
|
|
143
|
+
type: SysMcp
|
|
144
|
+
user_key: id
|
|
150
145
|
settings:
|
|
151
146
|
HomeJS: /admin-app/home.js
|
|
152
147
|
max_list_columns: 8
|
|
@@ -103,6 +103,27 @@
|
|
|
103
103
|
"console": "internalConsole",
|
|
104
104
|
"internalConsoleOptions": "openOnSessionStart"
|
|
105
105
|
},
|
|
106
|
+
{
|
|
107
|
+
"name": " - 1.5 GENAI - repaired-response=genai_demo.response_example",
|
|
108
|
+
"type": "debugpy",
|
|
109
|
+
"request": "launch",
|
|
110
|
+
"program": "${workspaceFolder}/venv/lib/python3.12/site-packages/api_logic_server_cli/cli.py",
|
|
111
|
+
"redirectOutput": true,
|
|
112
|
+
"cwd": "${workspaceFolder}",
|
|
113
|
+
"env": {
|
|
114
|
+
"PYTHONPATH": "",
|
|
115
|
+
"SECURITY_ENABLED": "False",
|
|
116
|
+
"PYTHONHASHSEED": "0",
|
|
117
|
+
"APILOGICSERVER_DEBUG": "False",
|
|
118
|
+
"OPT_LOCKING": "optional"},
|
|
119
|
+
"justMyCode": false,
|
|
120
|
+
"args": [ "genai", "--retries=-1"
|
|
121
|
+
, "--using=enai_demo.prompt"
|
|
122
|
+
, "--project-name=genai_demo"
|
|
123
|
+
, "--repaired-response=system/genai/examples/genai_demo/genai_demo.response_example"],
|
|
124
|
+
"console": "internalConsole",
|
|
125
|
+
"internalConsoleOptions": "openOnSessionStart"
|
|
126
|
+
},
|
|
106
127
|
{
|
|
107
128
|
"name": "ApiLogicServer Create",
|
|
108
129
|
"type": "debugpy",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
version info:
|
|
2
|
+
version info: 2.0 (05/24/2025)
|
|
3
3
|
---
|
|
4
|
-
## Welcome to
|
|
4
|
+
## Welcome to GenAI-Logic
|
|
5
5
|
|
|
6
6
|
1. ***Instant microservices*** (APIs and Admin Apps) from a database or **GenAI prompt** -- 1 command
|
|
7
7
|
|
|
@@ -79,7 +79,7 @@ Created projects use standard Flask and SQLAlchemy; automation is provided by Lo
|
|
|
79
79
|
als create --project-name=basic_demo --db-url=basic_demo
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
<br>The
|
|
82
|
+
<br>The basic_demo project provides:
|
|
83
83
|
1. A quick look at logic, security and integration
|
|
84
84
|
2. Integration includes Kafka and MCP (**[Model Context Protocol](https://apilogicserver.github.io/Docs/Integration-MCP/)**)
|
|
85
85
|
|
|
@@ -102,7 +102,7 @@ Then, try your own databases [(db-url examples here)](https://apilogicserver.git
|
|
|
102
102
|
|
|
103
103
|
<br>You can do this with or without signup:
|
|
104
104
|
|
|
105
|
-
1. If you have signed up (see *To obtain a ChatGPT API Key*, below), this will create
|
|
105
|
+
1. If you have signed up (see *To obtain a ChatGPT API Key*, below), this will create a new database and project called `genai_demo`, and open the project. It's created using `genai_demo.prompt`, visible in left Explorer pane:
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
108
|
als genai --using=system/genai/examples/genai_demo/genai_demo.prompt --project-name=genai_demo
|