You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
04_durable_rules/durable_rules_manual.ipynb

1542 lines
47 KiB
Plaintext

1 year ago
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"tags": []
},
"source": [
"# Durable Rules로 규칙 구현하기\n",
"- 산업인공지능학과 대학원\n",
" 2022254026\n",
" 김홍열"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"### Durable Rules 란?\n",
"* 비즈니스 룰 엔진을 구현하기 위한 라이브러리로 python, ruby, nodejs로 구현할 수 있다."
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"### Durable Rules 설치하기 (Python)\n",
"``` plantext\n",
"pip install durable_rules"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"### Durable Rules 사용법\n",
"[github - jruizgit/rules](https://github.com/jruizgit/rules)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"##### 패키지 불러오기"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from durable.lang import *"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Rules (Trigger: @when_all(==, &, <<))\n",
"* 규칙은 프레임워크의 기본 구성 요소입니다.\n",
"* 규칙의 선행 조건은 규칙의 결과 조건(동작)을 실행하기 위해 충족되어야 하는 조건을 정의합니다.\n",
"* 관례적으로 m은 주어진 규칙에 의해 평가될 데이터를 나타냅니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"with ruleset('test'):\n",
" # antecedent\n",
" @when_all(m.subject == 'World')\n",
" def say_hello(c):\n",
" # consequent\n",
" print ('Hello {0}'.format(c.m.subject))"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hello World\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"post('test', { 'subject': 'World' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Facts (assert_fact)\n",
"* 사실은 지식 기반을 정의하는 데이터를 나타냅니다.\n",
"* 사실은 JSON 객체로 주장되며, 취소될 때까지 저장됩니다.\n",
"* 사실이 규칙의 선행 조건을 만족하면, 규칙의 결과 조건이 실행됩니다."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"with ruleset('animal'):\n",
" # will be triggered by 'Kermit eats flies'\n",
" @when_all((m.predicate == 'eats') & (m.object == 'flies'))\n",
" def frog(c):\n",
" c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'frog' })\n",
"\n",
" @when_all((m.predicate == 'eats') & (m.object == 'worms'))\n",
" def bird(c):\n",
" c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'bird' })\n",
"\n",
" # will be chained after asserting 'Kermit is frog'\n",
" @when_all((m.predicate == 'is') & (m.object == 'frog'))\n",
" def green(c):\n",
" c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'green' })\n",
"\n",
" @when_all((m.predicate == 'is') & (m.object == 'bird'))\n",
" def black(c):\n",
" c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'black' })\n",
"\n",
" @when_all(+m.subject)\n",
" def output(c):\n",
" print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fact: Kermit is green\n",
"Fact: Kermit is frog\n",
"Fact: Kermit eats flies\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assert_fact('animal', { 'subject': 'Kermit', 'predicate': 'eats', 'object': 'flies' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Events (post)\n",
"* 이벤트는 규칙에 전달되어 평가될 수 있습니다. \n",
"* 이벤트란 일시적인 사실로, 결과를 실행하기 직전에 취소되는 사실입니다.\n",
"* 따라서 이벤트는 한 번만 관찰할 수 있습니다.\n",
"* 이벤트는 관찰될 때까지 저장됩니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"with ruleset('risk'):\n",
" @when_all(c.first << m.t == 'purchase',\n",
" c.second << m.location != c.first.location)\n",
" # the event pair will only be observed once\n",
" def fraud(c):\n",
" print('Fraud detected -> {0}, {1}'.format(c.first.location, c.second.location))"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fraud detected -> CA, US\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"post('risk', {'t': 'purchase', 'location': 'US'})\n",
"post('risk', {'t': 'purchase', 'location': 'CA'})"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"##### ✨위 예제에서 Event가 아닌 Fact를 적용하면 다음과 같이 출력됩니다."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Fraud detected -> CA, US\n",
"Fraud detected -> US, CA\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assert_fact('risk', {'t': 'purchase', 'location': 'US', 'last_location': None})\n",
"assert_fact('risk', {'t': 'purchase', 'location': 'CA', 'last_location': None})"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"``` plaintext\n",
"Fraud detected -> US, CA\n",
"Fraud detected -> CA, US\n",
"```"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"예에서 두 가지 사실 모두 첫 번째 조건인 m.t == 'purchase'를 충족하며, 각 사실은 첫 번째 조건을 충족한 사실과 관련하여 두 번째 조건인 m.location != c.first.location을 충족합니다.\n",
"\n",
"이벤트는 일시적인 사실입니다. 사실이 발송될 예정이라면 즉시 취소됩니다. 위 예제에서 post를 사용할 때, 두 번째 쌍이 계산되는 시점에 이미 이벤트가 취소되어 있습니다.\n",
"\n",
"발송 전에 이벤트를 취소함으로써 작업 실행 중 계산해야 할 조합의 수를 줄일 수 있습니다."
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### State (s, update_state)\n",
"* 규칙의 결과가 실행될 때 컨텍스트 상태를 사용할 수 있습니다. \n",
"* 동일한 컨텍스트 상태는 규칙 실행 간에 전달됩니다. \n",
"* 컨텍스트 상태는 삭제될 때까지 저장됩니다. \n",
"* 컨텍스트 상태 변경은 규칙에 의해 평가될 수 있습니다. \n",
"* 관례적으로 s는 규칙에 의해 평가되는 상태를 나타냅니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"with ruleset('flow'):\n",
" # state condition uses 's'\n",
" @when_all(s.status == 'start')\n",
" def start(c):\n",
" # state update on 's'\n",
" c.s.status = 'next' \n",
" print('start')\n",
"\n",
" @when_all(s.status == 'next')\n",
" def next(c):\n",
" c.s.status = 'last' \n",
" print('next')\n",
"\n",
" @when_all(s.status == 'last')\n",
" def last(c):\n",
" c.s.status = 'end' \n",
" print('last')\n",
" # deletes state at the end\n",
" c.delete_state()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"start\n",
"next\n",
"last\n"
]
}
],
"source": [
"update_state('flow', { 'status': 'start' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Identity (+속성, none(+속성))\n",
"* 같은 속성 이름과 값이 있는 팩트들은 단언(asserted)되거나 철회(retracted)될 때 동등하다고 간주됩니다.\n",
"* 같은 속성 이름과 값이 있는 이벤트들은 게시 시간이 중요하기 때문에 게시될 때 서로 다른 것으로 간주됩니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"with ruleset('bookstore'):\n",
" # this rule will trigger for events with status\n",
" @when_all(+m.status)\n",
" def event(c):\n",
" print('bookstore-> Reference {0} status {1}'.format(c.m.reference, c.m.status))\n",
"\n",
" @when_all(+m.name)\n",
" def fact(c):\n",
" print('bookstore-> Added {0}'.format(c.m.name))\n",
" \n",
" # this rule will be triggered when the fact is retracted\n",
" @when_all(none(+m.name))\n",
" def empty(c):\n",
" print('bookstore-> No books')\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"bookstore-> Added The new book\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# 단언(assert_fact)이 성공했기 때문에 예외를 발생시키지 않습니다. \n",
"assert_fact('bookstore', {\n",
" 'name': 'The new book',\n",
" 'seller': 'bookstore',\n",
" 'reference': '75323',\n",
" 'price': 500\n",
"})"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"bookstore expected Message has already been observed: {\"reference\": \"75323\", \"name\": \"The new book\", \"price\": 500, \"seller\": \"bookstore\"}\n"
]
}
],
"source": [
"# 이미 단언(assert_fact)된 사실이기 때문에 MessageObservedError가 발생합니다. \n",
"try:\n",
" assert_fact('bookstore', {\n",
" 'reference': '75323',\n",
" 'name': 'The new book',\n",
" 'price': 500,\n",
" 'seller': 'bookstore'\n",
" })\n",
"except BaseException as e:\n",
" print('bookstore expected {0}'.format(e.message))\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"bookstore-> Reference 75323 status Active\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# 새로운 이벤트가 게시되기 때문에 예외를 발생시키지 않습니다. \n",
"post('bookstore', {\n",
" 'reference': '75323',\n",
" 'status': 'Active'\n",
"})"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"bookstore-> Reference 75323 status Active\n"
]
}
],
"source": [
"# 새로운 이벤트가 게시되기 때문에 예외를 발생시키지 않습니다.\n",
"post('bookstore', {\n",
" 'reference': '75323',\n",
" 'status': 'Active'\n",
"})\n",
"\n",
"retract_fact('bookstore', {\n",
" 'reference': '75323',\n",
" 'name': 'The new book',\n",
" 'price': 500,\n",
" 'seller': 'bookstore'\n",
"})"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Pattern Matching"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### String Operations"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Correlated Sequence\n",
"* 규칙은 서로 관련된 이벤트 또는 사실의 시퀀스를 효율적으로 평가하는 데 사용할 수 있습니다. 아래 예시의 사기 탐지 규칙은 세 가지 이벤트 패턴을 보여줍니다: 두 번째 이벤트 금액이 첫 번째 이벤트 금액의 200%를 초과하고 세 번째 이벤트 금액이 다른 두 이벤트의 평균보다 큽니다.\n",
"* 기본적으로 관련된 시퀀스는 서로 다른 메시지를 캡처합니다. 아래 예시에서 두 번째 이벤트는 두 번째와 세 번째 조건을 모두 만족하지만, 이벤트는 두 번째 조건에 대해서만 캡처됩니다. distinct 속성을 사용하여 서로 다른 이벤트 또는 사실의 상관 관계를 비활성화할 수 있습니다.\n",
"* when_all 주석은 이벤트 또는 사실의 시퀀스를 표현합니다. << 연산자는 이후 표현식에서 참조할 수 있는 이벤트 또는 사실의 이름을 지정하는 데 사용됩니다. 이벤트 또는 사실을 참조할 때 모든 속성을 사용할 수 있습니다. 산술 연산자를 사용하여 복잡한 패턴을 표현할 수 있습니다.\n",
"* 산술 연산자: +, -, *, /"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"fraud detected -> 50\n",
" -> 251\n",
" -> 200\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('risk'):\n",
" @when_all(# distinct(True),\n",
" c.first << m.amount > 10,\n",
" c.second << m.amount > c.first.amount * 2,\n",
" c.third << m.amount > (c.first.amount + c.second.amount) / 2)\n",
" def detected(c):\n",
" print('fraud detected -> {0}'.format(c.first.amount))\n",
" print(' -> {0}'.format(c.second.amount))\n",
" print(' -> {0}'.format(c.third.amount))\n",
" \n",
"post('risk', { 'amount': 50 })\n",
"post('risk', { 'amount': 200 })\n",
"post('risk', { 'amount': 251 })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Choice of Sequences\n",
"* durable_rules는 보다 풍부한 이벤트 시퀀스를 표현하고 효율적으로 평가할 수 있게 해줍니다. 아래 예시에서 두 이벤트\\사실 시퀀스 각각이 동작을 실행합니다.\n",
"\n",
"* 다음 두 함수는 더 풍부한 이벤트 시퀀스를 정의하는 데 사용되고 결합할 수 있습니다:\n",
"\n",
"all: 이벤트 또는 사실 패턴의 집합입니다. 동작을 실행하려면 모든 패턴이 일치해야 합니다.\n",
"\n",
"any: 이벤트 또는 사실 패턴의 집합입니다. 어느 하나만 일치해도 동작이 실행됩니다."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Approved approve 1000\n",
"Approved jumbo 10000\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('expense'):\n",
" @when_any(all(c.first << m.subject == 'approve', \n",
" c.second << m.amount == 1000), \n",
" all(c.third << m.subject == 'jumbo', \n",
" c.fourth << m.amount == 10000))\n",
" def action(c):\n",
" if c.first:\n",
" print ('Approved {0} {1}'.format(c.first.subject, c.second.amount))\n",
" else:\n",
" print ('Approved {0} {1}'.format(c.third.subject, c.fourth.amount))\n",
" \n",
"\n",
"post('expense', { 'subject': 'approve' })\n",
"post('expense', { 'amount': 1000 })\n",
"post('expense', { 'subject': 'jumbo' })\n",
"post('expense', { 'amount': 10000 })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Lack of Information\n",
"* 일부 경우에는 정보 부족이 중요한 의미를 가집니다. none 함수는 관련된 시퀀스가 있는 규칙에서 정보 부족을 평가하는 데 사용할 수 있습니다.\n",
"\n",
"* 참고: none 함수는 정보 부족에 대한 추론을 위해 정보가 필요합니다. 즉, 해당 규칙에 이벤트나 사실이 등록되지 않은 경우에는 동작을 실행하지 않습니다."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"fraud detected deposit withdrawal chargeback\n",
"fraud detected deposit withdrawal chargeback\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '1', 'id': 'sid-1', '$s': 1}"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('risk'):\n",
" @when_all(c.first << m.t == 'deposit',\n",
" none(m.t == 'balance'),\n",
" c.third << m.t == 'withdrawal',\n",
" c.fourth << m.t == 'chargeback')\n",
" def detected(c):\n",
" print('fraud detected {0} {1} {2}'.format(c.first.t, c.third.t, c.fourth.t))\n",
" \n",
"assert_fact('risk', { 't': 'deposit' })\n",
"assert_fact('risk', { 't': 'withdrawal' })\n",
"assert_fact('risk', { 't': 'chargeback' })\n",
"\n",
"assert_fact('risk', { 'sid': 1, 't': 'balance' })\n",
"assert_fact('risk', { 'sid': 1, 't': 'deposit' })\n",
"assert_fact('risk', { 'sid': 1, 't': 'withdrawal' })\n",
"assert_fact('risk', { 'sid': 1, 't': 'chargeback' })\n",
"retract_fact('risk', { 'sid': 1, 't': 'balance' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Nested OBjects\n",
"* 중첩된 이벤트 또는 사실에 대한 질의도 지원됩니다.\n",
"* . 표기법은 중첩된 객체의 속성에 대한 조건을 정의하는 데 사용됩니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"bill amount ->100\n",
"account payment amount ->100\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('expense'):\n",
" # use the '.' notation to match properties in nested objects\n",
" @when_all(c.bill << (m.t == 'bill') & (m.invoice.amount > 50),\n",
" c.account << (m.t == 'account') & (m.payment.invoice.amount == c.bill.invoice.amount))\n",
" def approved(c):\n",
" print ('bill amount ->{0}'.format(c.bill.invoice.amount))\n",
" print ('account payment amount ->{0}'.format(c.account.payment.invoice.amount))\n",
" \n",
"# one level of nesting\n",
"post('expense', {'t': 'bill', 'invoice': {'amount': 100}})\n",
" \n",
"#two levels of nesting\n",
"post('expense', {'t': 'account', 'payment': {'invoice': {'amount': 100}}})"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Arrays"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"fraud 1 detected [150, 300, 450]\n",
"fraud 2 detected [{'amount': 200}, {'amount': 300}, {'amount': 450}]\n",
"fraud 3 detected ['one card', 'two cards', 'three cards']\n",
"fraud 4 detected [[10, 20, 30], [30, 40, 50], [10, 20]]\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('risk'):\n",
" # matching primitive array\n",
" @when_all(m.payments.allItems((item > 100) & (item < 500)))\n",
" def rule1(c):\n",
" print('fraud 1 detected {0}'.format(c.m.payments))\n",
"\n",
" # matching object array\n",
" @when_all(m.payments.allItems((item.amount < 250) | (item.amount >= 300)))\n",
" def rule2(c):\n",
" print('fraud 2 detected {0}'.format(c.m.payments))\n",
"\n",
" # pattern matching string array\n",
" @when_all(m.cards.anyItem(item.matches('three.*')))\n",
" def rule3(c):\n",
" print('fraud 3 detected {0}'.format(c.m.cards))\n",
"\n",
" # matching nested arrays\n",
" @when_all(m.payments.anyItem(item.allItems(item < 100)))\n",
" def rule4(c):\n",
" print('fraud 4 detected {0}'.format(c.m.payments))\n",
" \n",
"post('risk', {'payments': [ 150, 300, 450 ]})\n",
"post('risk', {'payments': [ { 'amount' : 200 }, { 'amount' : 300 }, { 'amount' : 450 } ]})\n",
"post('risk', {'cards': [ 'one card', 'two cards', 'three cards' ]})\n",
"post('risk', {'payments': [ [ 10, 20, 30 ], [ 30, 40, 50 ], [ 10, 20 ] ]}) "
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Facts and Events as rvalues\n",
"* 스칼라 값(문자열, 숫자 및 부울 값) 외에도 표현식의 오른쪽에서 관찰된 사실이나 이벤트를 사용할 수 있습니다."
]
},
{
"cell_type": "code",
1 year ago
"execution_count": null,
1 year ago
"metadata": {},
1 year ago
"outputs": [],
1 year ago
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('risk'):\n",
" # compares properties in the same event, this expression is evaluated in the client \n",
" @when_all(m.debit > m.credit * 2)\n",
" def fraud_1(c):\n",
" print('debit {0} more than twice the credit {1}'.format(c.m.debit, c.m.credit))\n",
"\n",
" # compares two correlated events, this expression is evaluated in the backend\n",
" @when_all(c.first << m.amount > 100,\n",
" c.second << m.amount > c.first.amount + m.amount / 2)\n",
" def fraud_2(c):\n",
" print('fraud detected ->{0}'.format(c.first.amount))\n",
" print('fraud detected ->{0}'.format(c.second.amount))\n",
" \n",
"post('risk', { 'debit': 220, 'credit': 100 })\n",
"post('risk', { 'debit': 150, 'credit': 100 })\n",
"post('risk', { 'amount': 200 })\n",
"post('risk', { 'amount': 500 })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"# Consequents"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Conflict Resolution\n",
"* 이벤트와 사실 평가는 여러 결과를 초래할 수 있습니다. pri (중요도) 함수를 사용하여 트리거 순서를 제어할 수 있습니다. 낮은 값의 작업이 먼저 실행됩니다. 모든 작업의 기본값은 0입니다.\n",
"\n",
"* 이 예시에서, 마지막 규칙이 가장 높은 우선순위를 가지고 있으므로 먼저 트리거됩니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"attributes P1 ->50\n",
"attributes P2 ->50\n",
"attributes P3 ->50\n",
"attributes P2 ->150\n",
"attributes P3 ->150\n",
"attributes P3 ->250\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('attributes'):\n",
" @when_all(pri(3), m.amount < 300)\n",
" def first_detect(c):\n",
" print('attributes P3 ->{0}'.format(c.m.amount))\n",
" \n",
" @when_all(pri(2), m.amount < 200)\n",
" def second_detect(c):\n",
" print('attributes P2 ->{0}'.format(c.m.amount))\n",
" \n",
" @when_all(pri(1), m.amount < 100)\n",
" def third_detect(c):\n",
" print('attributes P1 ->{0}'.format(c.m.amount))\n",
" \n",
"assert_fact('attributes', { 'amount': 50 })\n",
"assert_fact('attributes', { 'amount': 150 })\n",
"assert_fact('attributes', { 'amount': 250 })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Action Batches\n",
"* 많은 수의 이벤트 또는 사실이 결과를 만족시킬 때, 결과는 일괄적으로 전달될 수 있습니다.\n",
"\n",
"count: 동작을 예약하기 전에 규칙이 만족해야 하는 정확한 횟수를 정의합니다.\n",
"\n",
"cap: 동작을 예약하기 전에 규칙이 만족해야 하는 최대 횟수를 정의합니다.\n",
"\n",
"* 이 예시는 정확히 세 개의 승인을 일괄 처리하고 거절 수를 두 개로 제한합니다:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"approved [{'amount': 30}, {'amount': 20}, {'amount': 10}]\n",
"rejected [{'expense': {'amount': 400}, 'approval': {'review': True}}, {'expense': {'amount': 200}, 'approval': {'review': True}}]\n",
"rejected [{'expense': {'amount': 100}, 'approval': {'review': True}}]\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('expense'):\n",
" # this rule will trigger as soon as three events match the condition\n",
" @when_all(count(3), m.amount < 100)\n",
" def approve(c):\n",
" print('approved {0}'.format(c.m))\n",
"\n",
" # this rule will be triggered when 'expense' is asserted batching at most two results \n",
" @when_all(cap(2),\n",
" c.expense << m.amount >= 100,\n",
" c.approval << m.review == True)\n",
" def reject(c):\n",
" print('rejected {0}'.format(c.m))\n",
"\n",
"post_batch('expense', [{ 'amount': 10 },\n",
" { 'amount': 20 },\n",
" { 'amount': 100 },\n",
" { 'amount': 30 },\n",
" { 'amount': 200 },\n",
" { 'amount': 400 }])\n",
"assert_fact('expense', { 'review': True })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Async Actions\n",
"* 결과 동작은 비동기적일 수 있습니다. \n",
"* 동작이 완료되면 완료(complete) 함수를 호출해야 합니다. \n",
"* 기본적으로 동작은 5초 후에 포기된 것으로 간주됩니다. \n",
"* 이 값은 작업 함수에서 다른 숫자를 반환하거나 renew_action_lease를 호출함으로써 변경할 수 있습니다."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"first completed\n",
"second completed\n"
]
}
],
"source": [
"from durable.lang import *\n",
"import threading\n",
"\n",
"with ruleset('flow'):\n",
" timer = None\n",
"\n",
" def start_timer(time, callback):\n",
" timer = threading.Timer(time, callback)\n",
" timer.daemon = True \n",
" timer.start()\n",
"\n",
" @when_all(s.state == 'first')\n",
" # async actions take a callback argument to signal completion\n",
" def first(c, complete):\n",
" def end_first():\n",
" c.s.state = 'second' \n",
" print('first completed')\n",
"\n",
" # completes the action after 3 seconds\n",
" complete(None)\n",
" \n",
" start_timer(3, end_first)\n",
" \n",
" @when_all(s.state == 'second')\n",
" def second(c, complete):\n",
" def end_second():\n",
" c.s.state = 'third'\n",
" print('second completed')\n",
"\n",
" # completes the action after 6 seconds\n",
" # use the first argument to signal an error\n",
" complete(Exception('error detected'))\n",
"\n",
" start_timer(6, end_second)\n",
"\n",
" # overrides the 5 second default abandon timeout\n",
" return 10\n",
" \n",
"update_state('flow', { 'state': 'first' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Unhandled Exceptions\n",
"* 액션에서 예외가 처리되지 않은 경우, 예외는 컨텍스트 상태에 저장됩니다. \n",
"* 이를 통해 예외 처리 규칙을 작성할 수 있습니다."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"exception caught Unhandled Exception!, traceback [' File \"/config/.local/lib/python3.10/site-packages/durable/engine.py\", line 241, in run\\n self._func(c)\\n', ' File \"/tmp/ipykernel_18661/3255112604.py\", line 7, in first\\n raise Exception(\\'Unhandled Exception!\\')\\n']\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('flow'):\n",
" \n",
" @when_all(m.action == 'start')\n",
" def first(c):\n",
" raise Exception('Unhandled Exception!')\n",
"\n",
" # when the exception property exists\n",
" @when_all(+s.exception)\n",
" def second(c):\n",
" print(c.s.exception)\n",
" c.s.exception = None\n",
" \n",
"post('flow', { 'action': 'start' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"### Flow Structures"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Statechart\n",
"* 규칙은 상태도(statecharts)를 사용하여 구성할 수 있습니다. 상태도는 결정적 유한 오토마타(DFA)입니다. 상태 컨텍스트는 가능한 여러 상태 중 하나에 있으며, 이러한 상태 간에 조건부 전환을 가집니다.\n",
"\n",
"* 상태도 규칙:\n",
"\n",
"1. 상태도는 하나 이상의 상태를 가질 수 있습니다.\n",
"2. 상태도에는 초기 상태가 필요합니다.\n",
"3. 초기 상태는 들어오는 간선이 없는 정점으로 정의됩니다.\n",
"4. 상태는 0개 이상의 트리거를 가질 수 있습니다.\n",
"5. 상태는 0개 이상의 상태를 가질 수 있습니다 (중첩 상태 참조).\n",
"6. 트리거에는 목적지 상태가 있습니다.\n",
"7. 트리거는 규칙을 가질 수 있습니다 (부재는 상태 진입을 의미).\n",
"8. 트리거는 액션을 가질 수 있습니다."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"requesting approve amount 100\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1, 'running': True}"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with statechart('expense'):\n",
" # initial state 'input' with two triggers\n",
" with state('input'):\n",
" # trigger to move to 'denied' given a condition\n",
" @to('denied')\n",
" @when_all((m.subject == 'approve') & (m.amount > 1000))\n",
" # action executed before state change\n",
" def denied(c):\n",
" print ('denied amount {0}'.format(c.m.amount))\n",
" \n",
" @to('pending') \n",
" @when_all((m.subject == 'approve') & (m.amount <= 1000))\n",
" def request(c):\n",
" print ('requesting approve amount {0}'.format(c.m.amount))\n",
" \n",
" # intermediate state 'pending' with two triggers\n",
" with state('pending'):\n",
" @to('approved')\n",
" @when_all(m.subject == 'approved')\n",
" def approved(c):\n",
" print ('expense approved')\n",
" \n",
" @to('denied')\n",
" @when_all(m.subject == 'denied')\n",
" def denied(c):\n",
" print ('expense denied')\n",
" \n",
" # 'denied' and 'approved' are final states \n",
" state('denied')\n",
" state('approved')\n",
" \n",
"# events directed to default statechart instance\n",
"post('expense', { 'subject': 'approve', 'amount': 100 })\n",
"post('expense', { 'subject': 'approved' })\n",
"\n",
"# events directed to statechart instance with id '1'\n",
"post('expense', { 'sid': 1, 'subject': 'approve', 'amount': 100 })\n",
"post('expense', { 'sid': 1, 'subject': 'denied' })\n",
"\n",
"# events directed to statechart instance with id '2'\n",
"post('expense', { 'sid': 2, 'subject': 'approve', 'amount': 10000 })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"### Nested States"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"* 중첩 상태를 사용하면 컴팩트한 상태도를 작성할 수 있습니다. \n",
"* 컨텍스트가 중첩 상태에 있는 경우, 컨텍스트는 묵시적으로 주변 상태에도 있습니다. \n",
"* 상태도는 하위 상태 컨텍스트에서 모든 이벤트를 처리하려고 시도합니다. * 하위 상태가 이벤트를 처리하지 않으면, 이벤트는 자동으로 상위 상태 컨텍스트에서 처리됩니다."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"start process\n",
"continue processing\n",
"continue processing\n",
"cancel process\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1, 'running': True}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with statechart('worker'):\n",
" # super-state 'work' has two states and one trigger\n",
" with state('work'):\n",
" # sub-state 'enter' has only one trigger\n",
" with state('enter'):\n",
" @to('process')\n",
" @when_all(m.subject == 'enter')\n",
" def continue_process(c):\n",
" print('start process')\n",
" \n",
" with state('process'):\n",
" @to('process')\n",
" @when_all(m.subject == 'continue')\n",
" def continue_process(c):\n",
" print('continue processing')\n",
"\n",
" # the super-state trigger will be evaluated for all sub-state triggers\n",
" @to('canceled')\n",
" @when_all(m.subject == 'cancel')\n",
" def cancel(c):\n",
" print('cancel process')\n",
"\n",
" state('canceled')\n",
"\n",
"# will move the statechart to the 'work.process' sub-state\n",
"post('worker', { 'subject': 'enter' })\n",
"\n",
"# will keep the statechart to the 'work.process' sub-state\n",
"post('worker', { 'subject': 'continue' })\n",
"post('worker', { 'subject': 'continue' })\n",
"\n",
"# will move the statechart out of the work state\n",
"post('worker', { 'subject': 'cancel' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"### Flowchart"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"* 플로우차트는 규칙 세트 흐름을 구성하는 또 다른 방법입니다. 플로우차트에서 각 단계는 실행할 액션을 나타냅니다. 따라서 (상태도 상태와 달리) 컨텍스트 상태에 적용되면 다른 단계로 전환됩니다.\n",
"\n",
"* 플로우차트 규칙:\n",
"\n",
"1. 플로우차트는 하나 이상의 단계를 가질 수 있습니다.\n",
"2. 플로우차트에는 초기 단계가 필요합니다.\n",
"3. 초기 단계는 들어오는 간선이 없는 정점으로 정의됩니다.\n",
"4. 단계는 액션을 가질 수 있습니다.\n",
"5. 단계는 0개 이상의 조건을 가질 수 있습니다.\n",
"6. 조건에는 규칙과 목적지 단계가 있습니다."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"requesting approve\n"
]
},
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1, 'running': True}"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with flowchart('expense'):\n",
" # initial stage 'input' has two conditions\n",
" with stage('input'): \n",
" to('request').when_all((m.subject == 'approve') & (m.amount <= 1000))\n",
" to('deny').when_all((m.subject == 'approve') & (m.amount > 1000))\n",
" \n",
" # intermediate stage 'request' has an action and three conditions\n",
" with stage('request'):\n",
" @run\n",
" def request(c):\n",
" print('requesting approve')\n",
" \n",
" to('approve').when_all(m.subject == 'approved')\n",
" to('deny').when_all(m.subject == 'denied')\n",
" # reflexive condition: if met, returns to the same stage\n",
" to('request').when_all(m.subject == 'retry')\n",
" \n",
" with stage('approve'):\n",
" @run \n",
" def approved(c):\n",
" print('expense approved')\n",
"\n",
" with stage('deny'):\n",
" @run\n",
" def denied(c):\n",
" print('expense denied')\n",
"\n",
"# events for the default flowchart instance, approved after retry\n",
"post('expense', { 'subject': 'approve', 'amount': 100 })\n",
"# post('expense', { 'subject': 'retry' })\n",
"# post('expense', { 'subject': 'approved' })\n",
"\n",
"# # events for the flowchart instance '1', denied after first try\n",
"# post('expense', { 'sid': 1, 'subject': 'approve', 'amount': 100})\n",
"# post('expense', { 'sid': 1, 'subject': 'denied'})\n",
"\n",
"# # # event for the flowchart instance '2' immediately denied\n",
"# post('expense', { 'sid': 2, 'subject': 'approve', 'amount': 10000})"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Timer\n",
"* 이벤트는 타이머를 사용하여 예약할 수 있습니다. \n",
"* 시간 초과 조건은 규칙 전제에 포함될 수 있습니다.\n",
"* 기본적으로 타임아웃은 이벤트로 트리거됩니다 (한 번만 관찰됨).\n",
"* '수동 리셋' 타이머에 의해 타임아웃은 사실로도 트리거될 수 있으며, 액션 실행 중 타이머를 리셋할 수 있습니다 (마지막 예제 참조).\n",
"\n",
"start_timer: 지정된 이름과 지속 시간으로 타이머를 시작합니다 (manual_reset은 선택 사항입니다).\n",
"reset_timer: '수동 리셋' 타이머를 리셋합니다.\n",
"cancel_timer: 진행 중인 타이머를 취소합니다.\n",
"timeout: 전제 조건으로 사용됩니다."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'sid': '0', 'id': 'sid-0', '$s': 1}"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"timer timeout\n"
]
}
],
"source": [
"from durable.lang import *\n",
"\n",
"with ruleset('timer'):\n",
" \n",
" @when_all(m.subject == 'start')\n",
" def start(c):\n",
" c.start_timer('MyTimer', 5)\n",
" \n",
" @when_all(timeout('MyTimer'))\n",
" def timer(c):\n",
" print('timer timeout')\n",
"\n",
"post('timer', { 'subject': 'start' })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"* 아래 예제에서는 타이머를 사용하여 더 높은 이벤트 비율을 감지합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from durable.lang import *\n",
"\n",
"with statechart('risk'):\n",
" with state('start'):\n",
" @to('meter')\n",
" def start(c):\n",
" c.start_timer('RiskTimer', 5)\n",
"\n",
" with state('meter'):\n",
" @to('fraud')\n",
" @when_all(count(3), c.message << m.amount > 100)\n",
" def fraud(c):\n",
" for e in c.m:\n",
" print(e.message) \n",
"\n",
" @to('exit')\n",
" @when_all(timeout('RiskTimer'))\n",
" def exit(c):\n",
" print('exit')\n",
"\n",
" state('fraud')\n",
" state('exit')\n",
"\n",
"# three events in a row will trigger the fraud rule\n",
"post('risk', { 'amount': 200 })\n",
"post('risk', { 'amount': 300 })\n",
"post('risk', { 'amount': 400 })\n",
"\n",
"# two events will exit after 5 seconds\n",
"post('risk', { 'sid': 1, 'amount': 500 })\n",
"post('risk', { 'sid': 1, 'amount': 600 })"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"* 이 예제에서는 속도를 측정하기 위해 수동 리셋 타이머를 사용합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from durable.lang import *\n",
"\n",
"with statechart('risk'):\n",
" with state('start'):\n",
" @to('meter')\n",
" def start(c):\n",
" c.start_timer('VelocityTimer', 5, True)\n",
"\n",
" with state('meter'):\n",
" @to('meter')\n",
" @when_all(cap(5), \n",
" m.amount > 100,\n",
" timeout('VelocityTimer'))\n",
" def some_events(c):\n",
" print('velocity: {0} in 5 seconds'.format(len(c.m)))\n",
" # resets and restarts the manual reset timer\n",
" c.reset_timer('VelocityTimer')\n",
" c.start_timer('VelocityTimer', 5, True)\n",
"\n",
" @to('meter')\n",
" @when_all(pri(1), timeout('VelocityTimer'))\n",
" def no_events(c):\n",
" print('velocity: no events in 5 seconds')\n",
" c.reset_timer('VelocityTimer')\n",
" c.start_timer('VelocityTimer', 5, True)\n",
"\n",
"post('risk', { 'amount': 200 })\n",
"post('risk', { 'amount': 300 })\n",
"post('risk', { 'amount': 50 })\n",
"post('risk', { 'amount': 500 })\n",
"post('risk', { 'amount': 600 })"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.11"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}