ApiLogicServer 14.5.0__py3-none-any.whl → 14.5.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api_logic_server_cli/add_cust/add_cust.py +7 -21
- api_logic_server_cli/api_logic_server.py +4 -2
- api_logic_server_cli/api_logic_server_info.yaml +2 -2
- api_logic_server_cli/create_from_model/__pycache__/dbml.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/__pycache__/ont_build.cpython-312.pyc +0 -0
- api_logic_server_cli/create_from_model/dbml.py +3 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/{mcp_server_executor.py → mcp_discovery.py} +1 -43
- api_logic_server_cli/prototypes/basic_demo/customizations/config/server_setup.py +388 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/database/system/SAFRSBaseX.py +139 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/.DS_Store +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/README_mcp.md +3 -1
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py +144 -131
- api_logic_server_cli/prototypes/basic_demo/customizations/logic/declare_logic.py +22 -2
- api_logic_server_cli/prototypes/basic_demo/iteration/logic/declare_logic.py +1 -1
- api_logic_server_cli/prototypes/nw/logic/declare_logic.py +2 -2
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/app.json +1 -0
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/appearance.json +1 -0
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/core-plugins.json +31 -0
- api_logic_server_cli/prototypes/nw_no_cust/.obsidian/workspace.json +166 -0
- apilogicserver-14.5.4.dist-info/METADATA +168 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/RECORD +25 -43
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/WHEEL +1 -1
- api_logic_server_cli/prototypes/basic_demo/customizations/api/api_discovery/proper_update_def.json +0 -71
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/1_langchain_loader.py +0 -71
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/2_gpt_mcp_prompt.txt +0 -19
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_flow.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/multi_mcp_orchestration.yaml +0 -49
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/multi_mcp_flow/wny mcp flows.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/natlang_to_api.py +0 -73
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/curl.txt +0 -5
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP Overview.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Arch.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/images/MCP_Overview_Executor.png +0 -0
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/1 - prompt_messages_array.json +0 -10
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/invoke_llm/2 - completion_tool_context.json +0 -12
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/llm_schema.txt +0 -38
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_2.yaml +0 -17393
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3.yaml +0 -16660
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/nw_swagger_3_relaxed.yaml +0 -109
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_server.py +0 -51
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/proxy_serverZ.py +0 -72
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/resources/validate_jsonapi.py +0 -64
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/run_executor.py +0 -23
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/swagger_converter.py +0 -65
- api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/z_old/3_executor_test_agent.py +0 -52
- api_logic_server_cli/prototypes/manager/README_X.md +0 -663
- apilogicserver-14.5.0.dist-info/METADATA +0 -76
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/entry_points.txt +0 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/licenses/LICENSE +0 -0
- {apilogicserver-14.5.0.dist-info → apilogicserver-14.5.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
3
|
+
from sqlalchemy import Column, DECIMAL, Date, ForeignKey, Integer, String
|
|
4
|
+
from safrs import SAFRSBase
|
|
5
|
+
from flask_login import UserMixin
|
|
6
|
+
import safrs, flask_sqlalchemy
|
|
7
|
+
from safrs import jsonapi_attr
|
|
8
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import operator
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Base = declarative_base() # type: flask_sqlalchemy.model.DefaultMeta
|
|
15
|
+
#vh new x
|
|
16
|
+
@classmethod
|
|
17
|
+
def jsonapi_filter(cls):
|
|
18
|
+
"""
|
|
19
|
+
Use this to override SAFRS JSON:API filtering
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
_type_: SQLAlchemy query filter
|
|
23
|
+
"""
|
|
24
|
+
from sqlalchemy import text, or_, and_
|
|
25
|
+
from flask import request
|
|
26
|
+
expressions = []
|
|
27
|
+
sqlWhere = ""
|
|
28
|
+
query = cls._s_query
|
|
29
|
+
if args := request.args:
|
|
30
|
+
from api.system.expression_parser import advancedFilter
|
|
31
|
+
expressions, sqlWhere = advancedFilter(cls, args)
|
|
32
|
+
if sqlWhere != "":
|
|
33
|
+
return query.filter(text(sqlWhere))
|
|
34
|
+
else:
|
|
35
|
+
return query.filter(and_(*expressions))
|
|
36
|
+
|
|
37
|
+
class SAFRSBaseX(SAFRSBase, safrs.DB.Model):
|
|
38
|
+
__abstract__ = True
|
|
39
|
+
if do_enable_ont_advanced_filters := False:
|
|
40
|
+
jsonapi_filter = jsonapi_filter
|
|
41
|
+
|
|
42
|
+
def _s_parse_attr_value(self, attr_name: str, attr_val: any):
|
|
43
|
+
"""
|
|
44
|
+
Parse the given jsonapi attribute value so it can be stored in the db
|
|
45
|
+
:param attr_name: attribute name
|
|
46
|
+
:param attr_val: attribute value
|
|
47
|
+
:return: parsed value
|
|
48
|
+
"""
|
|
49
|
+
attr = self.__class__._s_jsonapi_attrs.get(attr_name, None)
|
|
50
|
+
if hasattr(attr, "type"): # pragma: no cover
|
|
51
|
+
|
|
52
|
+
if str(attr.type) in ["DATE", "DATETIME"] and attr_val:
|
|
53
|
+
try:
|
|
54
|
+
attr_val = attr_val.replace("T", " ")
|
|
55
|
+
datetime.strptime(attr_val, '%Y-%m-%d %H:%M')
|
|
56
|
+
attr_val += ":00"
|
|
57
|
+
except ValueError:
|
|
58
|
+
pass
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
safrs.log.warning(exc)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
return super()._s_parse_attr_value(attr_name, attr_val)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def _s_filter(cls, *filter_args, **filter_kwargs):
|
|
68
|
+
"""
|
|
69
|
+
Apply a filter to this model
|
|
70
|
+
:param filter_args: A list of filters information to apply, passed as a request URL parameter.
|
|
71
|
+
Each filter object has the following fields:
|
|
72
|
+
- name: The name of the field you want to filter on.
|
|
73
|
+
- op: The operation you want to use (all sqlalchemy operations are available). The valid values are:
|
|
74
|
+
- like: Invoke SQL like (or "ilike", "match", "notilike")
|
|
75
|
+
- eq: check if field is equal to something
|
|
76
|
+
- ge: check if field is greater than or equal to something
|
|
77
|
+
- gt: check if field is greater than to something
|
|
78
|
+
- ne: check if field is not equal to something
|
|
79
|
+
- is_: check if field is a value
|
|
80
|
+
- is_not: check if field is not a value
|
|
81
|
+
- le: check if field is less than or equal to something
|
|
82
|
+
- lt: check if field is less than to something
|
|
83
|
+
- val: The value that you want to compare.
|
|
84
|
+
:return: sqla query object
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
filters = json.loads(filter_args[0])
|
|
88
|
+
except json.decoder.JSONDecodeError:
|
|
89
|
+
raise ValidationError("Invalid filter format (see https://github.com/thomaxxl/safrs/wiki)")
|
|
90
|
+
|
|
91
|
+
if not isinstance(filters, list):
|
|
92
|
+
filters = [filters]
|
|
93
|
+
|
|
94
|
+
expressions = []
|
|
95
|
+
query = cls._s_query
|
|
96
|
+
|
|
97
|
+
for filt in filters:
|
|
98
|
+
if not isinstance(filt, dict):
|
|
99
|
+
safrs.log.warning(f"Invalid filter '{filt}'")
|
|
100
|
+
continue
|
|
101
|
+
attr_name = filt.get("name")
|
|
102
|
+
attr_val = filt.get("val")
|
|
103
|
+
if attr_name != "id" and attr_name not in cls._s_jsonapi_attrs:
|
|
104
|
+
raise ValidationError(f'Invalid filter "{filt}", unknown attribute "{attr_name}"')
|
|
105
|
+
|
|
106
|
+
op_name = filt.get("op", "").strip("_")
|
|
107
|
+
attr = cls._s_jsonapi_attrs[attr_name] if attr_name != "id" else cls.id
|
|
108
|
+
if op_name in ["in", "notin"]:
|
|
109
|
+
op = getattr(attr, op_name + "_")
|
|
110
|
+
query = query.filter(op(attr_val))
|
|
111
|
+
elif op_name in ["like", "ilike", "match", "notilike"] and hasattr(attr, "like"):
|
|
112
|
+
# => attr is Column or InstrumentedAttribute
|
|
113
|
+
like = getattr(attr, op_name)
|
|
114
|
+
expressions.append(like(attr_val))
|
|
115
|
+
elif not hasattr(operator, op_name):
|
|
116
|
+
raise ValidationError(f'Invalid filter "{filt}", unknown operator "{op_name}"')
|
|
117
|
+
else:
|
|
118
|
+
op = getattr(operator, op_name)
|
|
119
|
+
expressions.append(op(attr, attr_val))
|
|
120
|
+
|
|
121
|
+
if len(filters) > 1:
|
|
122
|
+
return query.filter(operator.and_(*expressions))
|
|
123
|
+
else:
|
|
124
|
+
return query.filter(*expressions)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestBase(Base):
|
|
128
|
+
__abstract__ = True
|
|
129
|
+
def __init__(self, *args, **kwargs):
|
|
130
|
+
for name, val in kwargs.items():
|
|
131
|
+
col = getattr(self.__class__, name)
|
|
132
|
+
if 'amount_total' == name:
|
|
133
|
+
debug_stop = 'stop'
|
|
134
|
+
if val is not None:
|
|
135
|
+
if str(col.type) in ["DATE", "DATETIME"]:
|
|
136
|
+
pass
|
|
137
|
+
else:
|
|
138
|
+
kwargs[name] = col.type.python_type(val)
|
|
139
|
+
return super().__init__(*args, **kwargs)
|
|
Binary file
|
|
@@ -10,4 +10,6 @@ Model Context Protocol is a way for:
|
|
|
10
10
|
|
|
11
11
|
* ***Corporate database participation*** in such flows, by making key functions available as MCP calls.
|
|
12
12
|
|
|
13
|
-
This example is [explained here](https://apilogicserver.github.io/Docs/Integration-MCP/).
|
|
13
|
+
This example is [explained here](https://apilogicserver.github.io/Docs/Integration-MCP/).
|
|
14
|
+
|
|
15
|
+
> Note: this sample uses multi-term filters. These are usually OR'd together, but this example requires AND. This is provided by `database/system/SAFRSBaseX.py` (see `return query.filter(operator.and_(*expressions)`) in `_s_filter()`), activated in `config/server_setup.py`.
|
api_logic_server_cli/prototypes/basic_demo/customizations/integration/mcp/mcp_client_executor.py
CHANGED
|
@@ -10,16 +10,11 @@ Notes:
|
|
|
10
10
|
* See: integration/mcp/README_mcp.md
|
|
11
11
|
* python api_logic_server_run.py
|
|
12
12
|
|
|
13
|
-
ToDo - email example is incomplete:
|
|
14
|
-
1. Add email event handler (ala nw_sample/logic/declare_logic.py#send_n8n_message())
|
|
15
|
-
2. And, respect the customer email_opt_out
|
|
16
|
-
3. Needs to use date range
|
|
17
|
-
4. Data incomplete
|
|
18
|
-
|
|
19
13
|
"""
|
|
20
14
|
|
|
21
15
|
import json
|
|
22
16
|
import os
|
|
17
|
+
import re
|
|
23
18
|
import openai
|
|
24
19
|
import requests
|
|
25
20
|
|
|
@@ -83,30 +78,20 @@ def discover_mcp_servers():
|
|
|
83
78
|
|
|
84
79
|
|
|
85
80
|
def get_user_nl_query():
|
|
86
|
-
""" Get the natural language query from the user.
|
|
81
|
+
""" Get the natural language query from the user.
|
|
82
|
+
Add instructions for the LLM to generate a tool context block.
|
|
83
|
+
|
|
84
|
+
"""
|
|
87
85
|
|
|
88
86
|
global test_type
|
|
89
|
-
# this doesn't work -- missing commands for mcp_server_executor....
|
|
90
|
-
default_request = "List the orders created more than 30 days ago, and post an email message to the order's customer offering a discount"
|
|
91
|
-
|
|
92
|
-
default_request = "List the orders created more than 30 days ago, and send a discount email to the customer for each one."
|
|
93
|
-
# date range? curl -X GET "http://localhost:5656/api/Order?filter=[{\’name\'}: {\’CreatedOn\’}, {\’op\’}: {\’gt\’}, {\’val\’}: {\’2022-05-14\’}]”
|
|
94
|
-
|
|
95
|
-
default_request = "List the orders for customer 5, and send a discount email to the customer for each one."
|
|
96
87
|
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- tool: 'json-api'
|
|
105
|
-
- JSON:API-compliant filtering (e.g., filter[CreatedOn][lt])
|
|
106
|
-
- Use {{ order.customer_id }} as a placeholder in the second step.
|
|
107
|
-
- Include method, url, query_params or body, headers, expected_output.
|
|
108
|
-
"""
|
|
109
|
-
return query
|
|
88
|
+
if len(sys.argv) > 1 and sys.argv[1] != 'go':
|
|
89
|
+
request = sys.argv[1]
|
|
90
|
+
else:
|
|
91
|
+
request = "List the unshipped orders created before 2023-07-14, and send a discount email to the customer for each one."
|
|
92
|
+
if test_type != 'orchestration':
|
|
93
|
+
request = "List customers with credit over 1000"
|
|
94
|
+
return request
|
|
110
95
|
|
|
111
96
|
|
|
112
97
|
def query_llm_with_nl(nl_query):
|
|
@@ -131,53 +116,63 @@ def query_llm_with_nl(nl_query):
|
|
|
131
116
|
|
|
132
117
|
# setup default tool_context to bypass LLM call and save 2-3 secs in testing
|
|
133
118
|
if test_type == 'orchestration': # orchestration: emails to pending orders
|
|
119
|
+
|
|
134
120
|
tool_context = \
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"
|
|
121
|
+
{
|
|
122
|
+
"tool_type": "json-api",
|
|
123
|
+
"schema_version": "1.0",
|
|
124
|
+
"base_url": "http://localhost:5656/api",
|
|
125
|
+
"resources": [
|
|
126
|
+
{
|
|
127
|
+
"path": "/Order",
|
|
128
|
+
"method": "GET",
|
|
129
|
+
"query_params": [
|
|
130
|
+
{
|
|
131
|
+
"name": "date_shipped",
|
|
132
|
+
"op": "eq",
|
|
133
|
+
"val": None
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "date_created",
|
|
137
|
+
"op": "lt",
|
|
138
|
+
"val": "2023-07-14"
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"headers": [],
|
|
142
|
+
"expected_output": []
|
|
156
143
|
},
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
144
|
+
{
|
|
145
|
+
"path": "/Email",
|
|
146
|
+
"method": "POST",
|
|
147
|
+
"body": {
|
|
148
|
+
"customer_id": "{{ order.customer_id }}",
|
|
149
|
+
"message": "You have a discount on your unshipped order."
|
|
150
|
+
},
|
|
151
|
+
"headers": [],
|
|
152
|
+
"expected_output": []
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
else: # simple get request - list customers with credit over 1000
|
|
164
157
|
tool_context = \
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
158
|
+
{
|
|
159
|
+
"tool_type": "json-api",
|
|
160
|
+
"schema_version": "1.0",
|
|
161
|
+
"base_url": "http://localhost:5656/api",
|
|
162
|
+
"resources": [
|
|
163
|
+
{
|
|
164
|
+
"path": "/Customer",
|
|
165
|
+
"method": "GET",
|
|
166
|
+
"query_params": [
|
|
167
|
+
{
|
|
168
|
+
"name": "credit_limit",
|
|
169
|
+
"op": "gt",
|
|
170
|
+
"val": "1000"
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
} # Call the OpenAI API to generate the tool context
|
|
181
176
|
if create_tool_context_from_llm: # saves 2-3 seconds...
|
|
182
177
|
response = openai.chat.completions.create(
|
|
183
178
|
model="gpt-4",
|
|
@@ -186,13 +181,17 @@ def query_llm_with_nl(nl_query):
|
|
|
186
181
|
)
|
|
187
182
|
|
|
188
183
|
tool_context_str = response.choices[0].message.content
|
|
184
|
+
tool_context_str_no_cr = tool_context_str.replace("\n", '') # convert single quotes to double quotes
|
|
189
185
|
try:
|
|
190
|
-
tool_context = json.loads(
|
|
186
|
+
tool_context = json.loads(tool_context_str_no_cr)
|
|
191
187
|
except json.JSONDecodeError:
|
|
192
188
|
print("Failed to decode JSON from response:", tool_context_str)
|
|
193
189
|
return None
|
|
194
190
|
|
|
195
|
-
print("\
|
|
191
|
+
print("\n\n2a. LLM request:\n", json.dumps(messages, indent=4))
|
|
192
|
+
print("\n2b. NL Query:\n", nl_query)
|
|
193
|
+
print("\n2c. schema_text: \n", json.dumps(json.loads(schema_text), indent=4)) #schema_text[0:100])
|
|
194
|
+
print("\n2d. generated tool context from LLM:\n", json.dumps(tool_context, indent=4))
|
|
196
195
|
return tool_context
|
|
197
196
|
|
|
198
197
|
|
|
@@ -213,71 +212,85 @@ def process_tool_context(tool_context):
|
|
|
213
212
|
|
|
214
213
|
def get_query_param_filter(query_params):
|
|
215
214
|
""" return json:api filter
|
|
215
|
+
|
|
216
|
+
eg
|
|
217
|
+
curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null},{"name":"CreatedOn","op":"lt","val":"2023-07-14"}]'
|
|
218
|
+
|
|
219
|
+
curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"gt","val":"2023-07-14"}]'
|
|
220
|
+
curl -qg 'http://localhost:5656/api/Order?filter=[{"name":"date_shipped","op":"eq","val":null}]'
|
|
221
|
+
curl -qg 'http://localhost:5656/api/Customer?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]'
|
|
216
222
|
|
|
217
|
-
query_params might be:
|
|
218
|
-
"query_params": {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
query_params might be simple:
|
|
224
|
+
"query_params": [ {"name": "credit_limit", "op": "gt", "val": "1000"} ]
|
|
225
|
+
==> ?filter=[{"name":"credit_limit","op":"gt","val":"1000"}]
|
|
226
|
+
|
|
227
|
+
or a list:
|
|
228
|
+
"query_params": [
|
|
229
|
+
{
|
|
230
|
+
"name": "date_shipped",
|
|
231
|
+
"op": "eq",
|
|
232
|
+
"val": None
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "date_created",
|
|
236
|
+
"op": "lt",
|
|
237
|
+
"val": "2023-07-14"
|
|
238
|
+
}
|
|
239
|
+
],
|
|
223
240
|
|
|
224
241
|
"""
|
|
225
|
-
|
|
226
|
-
if isinstance(query_params, dict):
|
|
227
|
-
for each_key, each_value in query_params.items():
|
|
228
|
-
if isinstance(each_value, dict):
|
|
229
|
-
for sub_key, sub_value in each_value.items():
|
|
230
|
-
query_param_filter += f"&{each_key}[{sub_key}]={sub_value}"
|
|
231
|
-
else:
|
|
232
|
-
query_param_filter += f"&{each_key}={each_value}"
|
|
233
|
-
# query_params = ''
|
|
234
|
-
elif isinstance(query_params, dict):
|
|
235
|
-
assert False, "Query Params dict tbd"
|
|
236
|
-
return query_param_filter
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if isinstance(tool_context, dict):
|
|
240
|
-
query_params = tool_context["query_params"]
|
|
241
|
-
query_param_filter = get_query_param_filter(query_params)
|
|
242
|
-
mcp_response = requests.get(
|
|
243
|
-
tool_context["url"],
|
|
244
|
-
headers=tool_context["headers"],
|
|
245
|
-
params=query_param_filter
|
|
246
|
-
)
|
|
247
|
-
elif isinstance(tool_context, list):
|
|
248
|
-
context_data = {}
|
|
242
|
+
|
|
249
243
|
added_rows = 0
|
|
250
244
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
query_param_filter = ''
|
|
246
|
+
assert isinstance(query_params, list), "Query Params filter expected to be a list"
|
|
247
|
+
query_param_filter = 'filter=' + str(query_params)
|
|
248
|
+
# use urlencode to convert to JSON:API format...
|
|
249
|
+
# val urllib.parse.quote() or urllib.parse.urlencode()
|
|
250
|
+
# tool instructions... filtering, email etc
|
|
251
|
+
query_param_filter = query_param_filter.replace("'", '"') # convert single quotes to double quotes
|
|
252
|
+
query_param_filter = query_param_filter.replace("None", 'null')
|
|
253
|
+
query_param_filter = query_param_filter.replace("date_created", 'CreatedOn') # TODO - why this name?
|
|
254
|
+
return query_param_filter # end get_query_param_filter
|
|
255
|
+
|
|
256
|
+
assert isinstance(tool_context, (dict, list)), "Tool context expected to be a dictionary"
|
|
257
|
+
context_data = {}
|
|
258
|
+
added_rows = 0
|
|
259
|
+
|
|
260
|
+
for each_block in tool_context["resources"]:
|
|
261
|
+
if True: # TODO - add check for "tool": "json-api"
|
|
262
|
+
if each_block["method"] == "GET":
|
|
263
|
+
query_param_filter = get_query_param_filter(each_block["query_params"])
|
|
264
|
+
headers = {"Content-Type": "application/vnd.api+json"}
|
|
265
|
+
if "headers" in each_block:
|
|
266
|
+
headers.update(each_block["headers"])
|
|
267
|
+
mcp_response = requests.get(
|
|
268
|
+
url = tool_context["base_url"] + each_block["path"],
|
|
269
|
+
headers=headers,
|
|
270
|
+
params=query_param_filter
|
|
271
|
+
)
|
|
272
|
+
context_data = mcp_response.json()['data'] # result rows...
|
|
273
|
+
elif each_block["method"] in ["POST"]:
|
|
274
|
+
for each_order in context_data:
|
|
275
|
+
url = tool_context["base_url"] + each_block["path"]
|
|
276
|
+
json_update_data = { 'data': {"type": "Email", 'attributes': {} } }
|
|
277
|
+
json_update_data_attributes = json_update_data["data"]["attributes"]
|
|
278
|
+
json_update_data_attributes["customer_id"] = context_data[0]['attributes']["customer_id"] # TODO - fix
|
|
279
|
+
json_update_data_attributes["message"] = each_block["body"]["message"]
|
|
280
|
+
# 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.'}}}}
|
|
281
|
+
headers = {"Content-Type": "application/vnd.api+json"}
|
|
282
|
+
if "headers" in each_block:
|
|
283
|
+
headers.update(each_block["headers"])
|
|
284
|
+
mcp_response = requests.post(
|
|
285
|
+
url=url,
|
|
286
|
+
headers=headers,
|
|
287
|
+
json=json_update_data
|
|
259
288
|
)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
add_rows = 0
|
|
263
|
-
for each_order in context_data:
|
|
264
|
-
url = each_block["url"]
|
|
265
|
-
json_update_data = { 'data': {"type": "Email", 'attributes': {} } }
|
|
266
|
-
json_update_data_attributes = json_update_data["data"]["attributes"]
|
|
267
|
-
json_update_data_attributes["customer_id"] = context_data[0]['attributes']["customer_id"]
|
|
268
|
-
json_update_data_attributes["message"] = each_block["body"]["message"]
|
|
269
|
-
# 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.'}}}}
|
|
270
|
-
mcp_response = requests.post(
|
|
271
|
-
url=url,
|
|
272
|
-
headers=each_block["headers"],
|
|
273
|
-
json=json_update_data
|
|
274
|
-
)
|
|
275
|
-
add_rows += 1
|
|
276
|
-
pass
|
|
277
|
-
else:
|
|
278
|
-
print("Invalid tool context format. Expected a dictionary or a list.")
|
|
279
|
-
return None
|
|
289
|
+
added_rows += 1
|
|
290
|
+
pass
|
|
280
291
|
print("\n3. MCP Server (als) Response:\n", mcp_response.text)
|
|
292
|
+
if added_rows > 0:
|
|
293
|
+
print(f"...Added {added_rows} rows to the database; last row (only) shown above.")
|
|
281
294
|
return mcp_response
|
|
282
295
|
|
|
283
296
|
|
|
@@ -9,6 +9,7 @@ import api.system.opt_locking.opt_locking as opt_locking
|
|
|
9
9
|
from security.system.authorization import Grant, Security
|
|
10
10
|
from logic.load_verify_rules import load_verify_rules
|
|
11
11
|
import integration.kafka.kafka_producer as kafka_producer
|
|
12
|
+
from integration.n8n.n8n_producer import send_n8n_message
|
|
12
13
|
import logging
|
|
13
14
|
|
|
14
15
|
app_logger = logging.getLogger(__name__)
|
|
@@ -32,7 +33,7 @@ def declare_logic():
|
|
|
32
33
|
discover_logic()
|
|
33
34
|
|
|
34
35
|
# Logic from GenAI: (or, use your IDE w/ code completion)
|
|
35
|
-
from database.models import Product, Order, Item, Customer
|
|
36
|
+
from database.models import Product, Order, Item, Customer, Email
|
|
36
37
|
|
|
37
38
|
# Ensure the customer's balance is less than their credit limit
|
|
38
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,6 +55,25 @@ def declare_logic():
|
|
|
54
55
|
|
|
55
56
|
# End Logic from GenAI
|
|
56
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
|
def handle_all(logic_row: LogicRow): # #als: TIME / DATE STAMPING, OPTIMISTIC LOCKING
|
|
59
79
|
"""
|
|
@@ -77,7 +97,7 @@ def declare_logic():
|
|
|
77
97
|
Grant.process_updates(logic_row=logic_row)
|
|
78
98
|
|
|
79
99
|
did_stamping = False
|
|
80
|
-
if enable_stamping :=
|
|
100
|
+
if enable_stamping := True: # #als: DATE / USER STAMPING
|
|
81
101
|
row = logic_row.row
|
|
82
102
|
if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
|
|
83
103
|
row.CreatedOn = datetime.datetime.now()
|
|
@@ -119,7 +119,7 @@ def declare_logic():
|
|
|
119
119
|
Grant.process_updates(logic_row=logic_row)
|
|
120
120
|
|
|
121
121
|
did_stamping = False
|
|
122
|
-
if enable_stamping :=
|
|
122
|
+
if enable_stamping := True: # #als: DATE / USER STAMPING
|
|
123
123
|
row = logic_row.row
|
|
124
124
|
if logic_row.ins_upd_dlt == "ins" and hasattr(row, "CreatedOn"):
|
|
125
125
|
row.CreatedOn = datetime.datetime.now()
|
|
@@ -148,8 +148,8 @@ def declare_logic():
|
|
|
148
148
|
"Order Date": row.OrderDate,
|
|
149
149
|
#"items": [row.OrderDetailList]
|
|
150
150
|
},
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
ins_upd_dlt="upd", wh_entity="Order",
|
|
152
|
+
msg="1. /Webhook integration.py: n8n, sending ready Order payload")
|
|
153
153
|
|
|
154
154
|
logic_row.log("send_order_to_shipping - sent order to shipping and N8N/sendgrid") # see in log
|
|
155
155
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file-explorer": true,
|
|
3
|
+
"global-search": true,
|
|
4
|
+
"switcher": true,
|
|
5
|
+
"graph": true,
|
|
6
|
+
"backlink": true,
|
|
7
|
+
"canvas": true,
|
|
8
|
+
"outgoing-link": true,
|
|
9
|
+
"tag-pane": true,
|
|
10
|
+
"properties": false,
|
|
11
|
+
"page-preview": true,
|
|
12
|
+
"daily-notes": true,
|
|
13
|
+
"templates": true,
|
|
14
|
+
"note-composer": true,
|
|
15
|
+
"command-palette": true,
|
|
16
|
+
"slash-command": false,
|
|
17
|
+
"editor-status": true,
|
|
18
|
+
"bookmarks": true,
|
|
19
|
+
"markdown-importer": false,
|
|
20
|
+
"zk-prefixer": false,
|
|
21
|
+
"random-note": false,
|
|
22
|
+
"outline": true,
|
|
23
|
+
"word-count": true,
|
|
24
|
+
"slides": false,
|
|
25
|
+
"audio-recorder": false,
|
|
26
|
+
"workspaces": false,
|
|
27
|
+
"file-recovery": true,
|
|
28
|
+
"publish": false,
|
|
29
|
+
"sync": true,
|
|
30
|
+
"webviewer": false
|
|
31
|
+
}
|