ApiLogicServer 15.0.0__py3-none-any.whl → 15.0.9__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 (36) hide show
  1. api_logic_server_cli/add_cust/add_cust.py +8 -2
  2. api_logic_server_cli/api_logic_server.py +2 -1
  3. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  4. api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
  5. api_logic_server_cli/create_from_model/dbml.py +1 -0
  6. api_logic_server_cli/genai/genai_svcs.py +5 -2
  7. api_logic_server_cli/prototypes/base/api/api_discovery/mcp_discovery.py +63 -24
  8. api_logic_server_cli/prototypes/base/config/logging.yml +5 -0
  9. api_logic_server_cli/prototypes/base/config/server_setup.py +73 -0
  10. api_logic_server_cli/prototypes/base/integration/mcp/examples/mcp_discovery_response.json +150 -0
  11. api_logic_server_cli/prototypes/base/integration/mcp/examples/mcp_request.prompt +46 -0
  12. api_logic_server_cli/prototypes/base/integration/mcp/examples/mcp_tool_context_response.json +34 -0
  13. api_logic_server_cli/prototypes/base/integration/mcp/examples/mcp_tool_context_response_get.json +18 -0
  14. api_logic_server_cli/prototypes/base/integration/mcp/mcp_client_executor.py +129 -275
  15. api_logic_server_cli/prototypes/basic_demo/customizations/logic/logic_discovery/mcp_client_executor_request.py +11 -282
  16. api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/admin.yaml +3 -3
  17. api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/home.js +48 -0
  18. api_logic_server_cli/prototypes/manager/system/genai/mcp_learning/mcp.prompt +12 -0
  19. {apilogicserver-15.0.0.dist-info → apilogicserver-15.0.9.dist-info}/METADATA +1 -1
  20. {apilogicserver-15.0.0.dist-info → apilogicserver-15.0.9.dist-info}/RECORD +25 -31
  21. api_logic_server_cli/prototypes/base/integration/mcp/README_mcp.md +0 -15
  22. api_logic_server_cli/prototypes/base/integration/mcp/test_notes.txt +0 -37
  23. api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/mcp_discovery.py +0 -96
  24. api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +0 -388
  25. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
  26. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +0 -15
  27. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/Zmcp_client_executor.py +0 -294
  28. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_schema.txt +0 -47
  29. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_server_discovery.json +0 -9
  30. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_tool_context.json +0 -25
  31. api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/test_notes.txt +0 -37
  32. /api_logic_server_cli/prototypes/base/integration/mcp/{mcp_schema.txt → examples/mcp_schema.txt} +0 -0
  33. {apilogicserver-15.0.0.dist-info → apilogicserver-15.0.9.dist-info}/WHEEL +0 -0
  34. {apilogicserver-15.0.0.dist-info → apilogicserver-15.0.9.dist-info}/entry_points.txt +0 -0
  35. {apilogicserver-15.0.0.dist-info → apilogicserver-15.0.9.dist-info}/licenses/LICENSE +0 -0
  36. {apilogicserver-15.0.0.dist-info → apilogicserver-15.0.9.dist-info}/top_level.txt +0 -0
@@ -1,296 +1,150 @@
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
-
1
+ #!/usr/bin/env python3
2
+
3
+ ###############################################################################
4
+ #
5
+ # This file initializes and starts the API Logic Server, e.g.:
6
+ # $ Use your IDE Run Configurations (for debug)
7
+ # $ sh run.sh
8
+ # $ python3 api_logic_server_run.py [--help]
9
+ # $ gunicorn --log-level=info -b 0.0.0.0:5656 -w2 --reload api_logic_server_run:flask_app
10
+ #
11
+ # Then, access the Admin App and API via the Browser, eg:
12
+ # http://localhost:5656
13
+ #
14
+ # You typically do not customize this file.
15
+ #
16
+ # (v 15.00.08, June 07, 2025 20:31:07)
17
+ #
18
+ # See Main Code (at end).
19
+ # Use log messages to understand API and Logic activation.
20
+ #
21
+ ###############################################################################
22
+
23
+ api_logic_server__version = '15.00.08'
24
+ api_logic_server_created__on = 'June 07, 2025 20:31:07'
25
+ api_logic_server__host = 'localhost'
26
+ api_logic_server__port = '5656'
27
+
28
+ start_up_message = "normal start"
29
+
30
+ import os, logging, logging.config, sys, yaml # failure here means venv probably not set
31
+ from flask_sqlalchemy import SQLAlchemy
15
32
  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
- if query_actual == '':
94
- query_actual = "list customers with balance over 100."
95
- return query_actual + ";\n\n" + training_prompt
96
-
33
+ from pathlib import Path
34
+ from config.config import Args # sets up logging
35
+ from config import server_setup
97
36
 
98
- def query_llm_with_nl(schema_text, nl_query):
99
- """
100
- Query the LLM with a natural language query and schema text to generate a tool context block.
37
+ current_path = os.path.abspath(os.path.dirname(__file__))
38
+ sys.path.append(current_path)
39
+ project_dir = str(current_path)
40
+ project_name = os.path.basename(os.path.normpath(current_path))
101
41
 
102
- It handles both orchestration and simple GET requests.
103
- """
42
+ if server_setup.is_docker():
43
+ sys.path.append(os.path.abspath('/home/api_logic_server'))
104
44
 
105
- global test_type, create_tool_context_from_llm
45
+ logic_alerts = True
46
+ """ Set False to silence startup message """
47
+ declare_logic_message = ""
48
+ declare_security_message = "ALERT: *** Security Not Enabled ***"
106
49
 
107
- content = f"Natural language query:\n {nl_query}\nSchema:\n{schema_text}"
108
- messages = [
109
- {
110
- "role": "system",
111
- "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."
112
- },
113
- {
114
- "role": "user",
115
- "content": f"{content}"
116
- }
117
- ]
50
+ os.chdir(project_dir) # so admin app can find images, code
51
+ import api.system.api_utils as api_utils
52
+ logic_logger_activate_debug = False
53
+ """ True prints all rules on startup """
118
54
 
119
- request_print = content[0:1200] + '\n... etc' # limit for readability
120
- print("\n\n2a. LLM request:\n", request_print)
121
- # print("\n2b. NL Query:\n", nl_query)
122
- # print("\n2c. schema_text: (truncated) \n")
123
- # schema_print = json.dumps(json.loads(schema_text), indent=4)[:400] # limit for readability
124
- # print(schema_print)
55
+ from typing import TypedDict
56
+ import safrs # fails without venv - see https://apilogicserver.github.io/Docs/Project-Env/
57
+ from safrs import ValidationError, SAFRSBase, SAFRSAPI as _SAFRSAPI
58
+ from logic_bank.logic_bank import LogicBank
59
+ from logic_bank.exec_row_logic.logic_row import LogicRow
60
+ from logic_bank.rule_type.constraint import Constraint
61
+ from sqlalchemy.ext.declarative import declarative_base
62
+ from sqlalchemy.orm import Session
63
+ import socket
64
+ import warnings
65
+ from flask import Flask, redirect, send_from_directory, send_file
66
+ from flask_cors import CORS
67
+ import ui.admin.admin_loader as AdminLoader
68
+ from security.system.authentication import configure_auth
69
+ import oracledb
125
70
 
126
- if create_tool_context_from_llm: # takes 2-3 seconds...
127
- response = openai.chat.completions.create(
128
- model="gpt-4",
129
- messages=messages,
130
- temperature=0.2
131
- )
71
+ if os.getenv("EXPERIMENT") == '+':
72
+ app_logger = logging.getLogger("api_logic_server_app")
73
+ else:
74
+ app_logger = server_setup.logging_setup()
132
75
 
133
- tool_context_str = response.choices[0].message.content
134
- tool_context_str_no_cr = tool_context_str.replace("\n", '') # convert single quotes to double quotes
135
- try:
136
- tool_context = json.loads(tool_context_str_no_cr)
137
- except json.JSONDecodeError:
138
- print("Failed to decode JSON from response:", tool_context_str)
139
- return None
140
76
 
141
- print("\n2d. generated tool context from LLM:\n", json.dumps(tool_context, indent=4))
142
77
 
143
- if "resources" not in tool_context:
144
- raise ConstraintException("GenAI Error - LLM response does not contain 'resources'.")
145
- return tool_context
78
+ # ==================================
79
+ # MAIN CODE
80
+ # ==================================
146
81
 
82
+ flask_app = Flask("API Logic Server", template_folder='ui/templates') # templates to load ui/admin/admin.yaml
147
83
 
148
- def process_tool_context(tool_context):
149
- """ Process the orchestration request by executing multiple tool context blocks.
150
- This executes the tool context blocks against a live JSON:API server.
151
- It handles both GET and POST requests, and it can
152
- orchestrate multiple requests based on the provided tool context.
84
+ CORS(flask_app, resources=[{r"/api/*": {"origins": "*"}},{r"/ontimizeweb/*": {"origins": "*"}}],
85
+ allow_headers=["Content-Type", "Authorization", "Access-Control-Allow-Credentials"],supports_credentials=True)
153
86
 
154
- Note the orchestration is processed by the client executor (here), not the server executor.
87
+ args = server_setup.get_args(flask_app) # creation defaults
155
88
 
156
- Research:
89
+ import config.config as config
90
+ flask_app.config.from_object(config.Config)
91
+ app_logger.debug(f"\nConfig args: \n{args}") # config file (e.g., db uri's)
157
92
 
158
- 1. How is this a "USB", since the request was specific about JSON:API?
159
- 2. How is it clear to loop through the tool_context[0] and call tool_context[1]?
160
- """
161
- global server_url
93
+ args.get_cli_args(dunder_name=__name__, args=args)
94
+ app_logger.debug(f"\nCLI args: \n{args}") # api_logic_server_run cl args
162
95
 
163
- def get_query_param_filter(query_params):
164
- """ return json:api filter
96
+ flask_app.config.from_prefixed_env(prefix="APILOGICPROJECT") # env overrides (e.g., docker)
97
+ app_logger.debug(f"\nENV args: \n{args}\n\n")
165
98
 
166
- eg
167
- curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null},{"name":"CreatedOn","op":"lt","val":"2023-07-14"}]'
99
+ server_setup.validate_db_uri(flask_app)
168
100
 
169
- curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"gt","val":"2023-07-14"}]'
170
- curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null}]'
171
- curl -qg 'http://localhost:5656/api/Customer?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]'
172
-
173
- query_params might be simple:
174
- "query_params": [ {"name": "credit_limit", "op": "gt", "val": "1000"} ]
175
- ==> ?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]
176
-
177
- or a list:
178
- "query_params": [
179
- {
180
- "name": "date_shipped",
181
- "op": "eq",
182
- "val": None
183
- },
184
- {
185
- "name": "date_created",
186
- "op": "lt",
187
- "val": "2023-07-14"
188
- }
189
- ],
190
-
191
- """
192
-
193
- added_rows = 0
194
-
195
- query_param_filter = ''
196
- assert isinstance(query_params, list), "Query Params filter expected to be a list"
197
- query_param_filter = 'filter=' + str(query_params)
198
- # use urlencode to convert to JSON:API format...
199
- # val urllib.parse.quote() or urllib.parse.urlencode()
200
- # tool instructions... filtering, email etc "null"
201
- query_param_filter = query_param_filter.replace("'", '"') # convert single quotes to double quotes
202
- query_param_filter = query_param_filter.replace("None", 'null')
203
- query_param_filter = query_param_filter.replace('"null"', 'null')
204
- # query_param_filter = query_param_filter.replace("date_created", 'CreatedOn') # TODO - why this name?
205
- return query_param_filter # end get_query_param_filter
206
-
207
- def move_fields(src: dict, dest: dict, context_data: dict):
208
- """ Move fields from src to dest, replacing any variables with their values from context_data."""
209
- for variable_name, value in src.items():
210
- move_value = value
211
- if move_value.startswith("{") and move_value.endswith("}"):
212
- # strip the braces, and get the name after the first dot, # eg: "{Order.customer_id}" ==> "customer_id"``
213
- move_name = move_value[1:-1] # strip the braces
214
- if '.' in move_value:
215
- move_name = move_name.split('.', 1)[1]
216
- move_value = context_data['attributes'][move_name]
217
- dest[variable_name] = move_value
218
- return dest
219
-
220
- def print_get_response(query_param_filter, mcp_response):
221
- """ Print the response from the GET request. """
222
- print("\n3. MCP Server (als) GET filter(query_param_filter):\n", query_param_filter)
223
- print(" GET Response:\n", mcp_response.text)
224
- results : List[Dict] = mcp_response.json()['data']
225
- # print results in a table format
226
- if results:
227
- # Get all unique keys from all result dicts
228
- keys = set()
229
- for row in results:
230
- if isinstance(row, dict):
231
- keys.update(row.keys())
232
- keys = list(keys)
233
- # Print header
234
- print("\n| " + " | ".join(keys) + " |")
235
- print("|" + "|".join(["---"] * len(keys)) + "|")
236
- # Print rows
237
- for row in results:
238
- print("| " + " | ".join(str(row.get(k, "")) for k in keys) + " |")
239
- else:
240
- print("No results found.")
241
-
242
- assert isinstance(tool_context, (dict, list)), "Tool context expected to be a dictionary"
243
- context_data = {}
244
- added_rows = 0
245
-
246
- for each_block in tool_context["resources"]:
247
- if process_tool_context := True:
248
- if each_block["method"] == "GET":
249
- query_param_filter = get_query_param_filter(each_block["query_params"])
250
- headers = {"Content-Type": "application/vnd.api+json"}
251
- if "headers" in each_block:
252
- headers.update(each_block["headers"])
253
- mcp_response = requests.get(
254
- url = each_block["base_url"] + each_block["path"],
255
- headers=headers,
256
- params=query_param_filter
257
- )
258
- context_data = mcp_response.json()['data'] # result rows...
259
- print_get_response(query_param_filter, mcp_response)
260
- elif each_block["method"] in ["POST"]:
261
- for each_order in context_data:
262
- url = each_block["base_url"] + each_block["path"]
263
- json_update_data = { 'data': {"type": "Email", 'attributes': {} } }
264
- json_update_data_attributes = json_update_data["data"]["attributes"]
265
- move_fields( src= each_block["body"], dest=json_update_data_attributes, context_data=each_order)
266
- # 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.'}}}}
267
- headers = {"Content-Type": "application/vnd.api+json"}
268
- if "headers" in each_block:
269
- headers.update(each_block["headers"])
270
- mcp_response = requests.post(
271
- url=url,
272
- headers=headers,
273
- json=json_update_data
274
- )
275
- added_rows += 1
276
- pass
277
- print("\n3. MCP Server (als) POST Response:\n", mcp_response.text)
278
- if added_rows > 0:
279
- print(f"...Added {added_rows} rows to the database; last row (only) shown above.")
280
- return mcp_response
101
+ server_setup.api_logic_server_setup(flask_app, args)
281
102
 
103
+ AdminLoader.admin_events(flask_app = flask_app, args = args, validation_error = ValidationError)
282
104
 
283
105
  if __name__ == "__main__":
284
-
285
- # to run: Run Config > Run designated Python file
286
-
287
- schema_text = discover_mcp_servers() # see: 1-discovery-from-als
288
-
289
- query = "list customers with balance over 100"
290
- prompt = get_user_nl_query_and_training(query) # set breakpoint here, view log, then step
291
-
292
- tool_context = query_llm_with_nl(schema_text, prompt) # see: 2-tool-context-from-LLM
293
-
294
- mcp_response = process_tool_context(tool_context) # see: 3-MCP-server response
295
-
296
- print("\nTest complete.\n")
106
+ msg = f'API Logic Project loaded (not WSGI), version: 15.00.08\n'
107
+ msg += f'.. startup message: {start_up_message}\n'
108
+ if server_setup.is_docker():
109
+ msg += f' (running from docker container at flask_host: {args.flask_host} - may require refresh)\n'
110
+ else:
111
+ msg += f' (running locally at flask_host: {args.flask_host})\n'
112
+ app_logger.info(f'\n{msg}')
113
+
114
+ if args.create_and_run:
115
+ app_logger.info(f'==> Customizable API Logic Project created and running:\n'
116
+ f'..Open it with your IDE at {project_dir}\n')
117
+
118
+ start_up_message = f'{args.http_scheme}://{args.swagger_host}:{args.port} *'
119
+ if os.getenv('CODESPACES'):
120
+ app_logger.info(f'API Logic Project (name: {project_name}) starting on Codespaces:\n'
121
+ f'..Explore data and API on codespaces, swagger_host: {args.http_scheme}://{args.swagger_host}/\n')
122
+ start_up_message = f'{args.http_scheme}://{args.swagger_host}'
123
+ else:
124
+ app_logger.info(f'API Logic Project (name: {project_name}) starting:\n'
125
+ f'..Explore data and API at http_scheme://swagger_host:port {start_up_message}\n'
126
+ f'.... with flask_host: {args.flask_host}\n'
127
+ f'.... and swagger_port: {args.swagger_port}')
128
+ if logic_alerts:
129
+ app_logger.info(f'\nAlert: These following are **Critical** to unlocking value for project: {project_name}:')
130
+ app_logger.info(f'.. see logic.declare_logic.py -- {server_setup.declare_logic_message}')
131
+ app_logger.info(f'.. see security.declare_security.py -- {server_setup.declare_security_message}\n\n')
132
+
133
+ app_logger.info(f'*************************************************************************')
134
+ app_logger.info(f'* Startup Instructions: Open your Browser at: {start_up_message}')
135
+ app_logger.info(f'*************************************************************************\n')
136
+
137
+ flask_app.run(host=args.flask_host, threaded=True, port=args.port)
138
+ else:
139
+ msg = f'API Logic Project Loaded (WSGI), version 15.00.08\n'
140
+ msg += f'.. startup message: {start_up_message}\n'
141
+
142
+ if server_setup.is_docker():
143
+ msg += f' (running from docker container at {args.flask_host} - may require refresh)\n'
144
+ else:
145
+ msg += f' (running locally at flask_host: {args.flask_host})\n'
146
+ app_logger.info(f'\n{msg}')
147
+ app_logger.info(f'API Logic Project (name: {project_name}) starting:\n'
148
+ f'..Explore data and API at http_scheme://swagger_host:port {args.http_scheme}://{args.swagger_host}:{args.port}\n'
149
+ f'.... with flask_host: {args.flask_host}\n'
150
+ f'.... and swagger_port: {args.swagger_port}')