From 9f3b8a616da10c29d8be6102bbd58620c37e9598 Mon Sep 17 00:00:00 2001 From: Tomasz Rewak Date: Mon, 15 May 2023 18:11:18 +0200 Subject: [PATCH] AI and personalities --- ai.py | 61 +++++++++++++++++++++++++++++ assets/style.css | 87 +++++++++++++++++++++++++++++++++++++++++- components/response.js | 45 ++++++++++++++++++++++ components/wise_man.js | 21 +++++----- main.py | 76 +++++++++++++++++++++++++++++------- 5 files changed, 266 insertions(+), 24 deletions(-) create mode 100644 ai.py create mode 100644 components/response.js diff --git a/ai.py b/ai.py new file mode 100644 index 0000000..2e33c67 --- /dev/null +++ b/ai.py @@ -0,0 +1,61 @@ +import openai +import re + + +def is_yes_or_no_question(question: str, key: str): + openai.api_key = key + response = openai.ChatCompletion.create( + model='gpt-3.5-turbo', + messages=[ + {'role': 'system', 'content': 'You answer questions with a simple "yes" or "no".'}, + {'role': 'user', 'content': f'Can the following question be generally answered with a "yes" or "no"? \n {question}'}, + ] + ) + + content = response['choices'][0]['message']['content'] + + if re.match('^\W*yes\W*$', content, re.IGNORECASE): + return True + + if re.match('^\W*no\W*$', content, re.IGNORECASE): + return False + + raise Exception(f'Invalid question annotation response: {content}') + + +def get_answer(question: str, personality: str, key: str): + openai.api_key = key + response = openai.ChatCompletion.create( + model='gpt-3.5-turbo', + messages=[ + {'role': 'system', 'content': personality}, + {'role': 'system', 'content': 'Your answers are rather concise.'}, + {'role': 'user', 'content': question}, + ] + ) + + return response['choices'][0]['text'] + + +def classify_answer(question: str, personality: str, answer: str, key: str): + openai.api_key = key + response = openai.ChatCompletion.create( + model='gpt-3.5-turbo', + messages=[ + {'role': 'system', 'content': personality}, + {'role': 'system', 'content': 'Your answers are rather concise.'}, + {'role': 'user', 'content': question}, + {'role': 'system', 'content': answer}, + {'role': 'user', 'content': 'Summarize you answer with a simple "yes" or "no" (answering with a single word). If (and only if) that\'s not possible, instead of answering with "yes" or "no", list conditions under which the answer would be "yes".'}, + ] + ) + + content = response['choices'][0]['message']['content'] + + if re.match('^\W*yes\W*$', content, re.IGNORECASE): + return {'classification': True, 'conditions': None} + + if re.match('^\W*no\W*$', content, re.IGNORECASE): + return {'classification': False, 'conditions': None} + + return {'classification': True, 'conditions': content} diff --git a/assets/style.css b/assets/style.css index 30431e6..b8ffec0 100644 --- a/assets/style.css +++ b/assets/style.css @@ -50,11 +50,48 @@ input { .magi>.system-status { grid-area: 3 / 2 / 4 / 3; - font-family: Helvetica; + font-family: 'Helvetica'; + font-family: 'Lucida Console'; color: #ff8d00; } +.magi>.system-status>div { + font-size: 3cqh; + margin-left: 4cqw; + transform: scaleX(1.2); + transform-origin: left; +} + +.magi>.system-status>div:first-child { + font-size: 6cqh; + margin-left: 0; +} + +.magi>.response { + grid-area: 3 / 6 / 4 / 7; + justify-self: flex-end; + align-self: center; + border: solid 2px; + padding: 2px; +} + +.magi>.response>.inner { + white-space: nowrap; + border: solid 2px; + padding: 2px 10px; + font-size: 8cqh; + font-weight: bold; +} + .magi>.wise-man { + display: flex; + background: #ff8d00; + padding: 2px; +} + +.magi>.wise-man>.inner { + width: 100%; + height: 100%; font-family: Helvetica; font-size: 8cqh; font-weight: bold; @@ -65,15 +102,63 @@ input { .magi>.wise-man.melchior { grid-area: 4 / 5 / 6 / 7; +} + +.magi>.wise-man.melchior, +.magi>.wise-man.melchior>.inner { clip-path: polygon(35% 0, 100% 0, 100% 100%, 0 100%, 0 44%) } .magi>.wise-man.balthasar { grid-area: 2 / 3 / 5 / 6; +} + +.magi>.wise-man.balthasar, +.magi>.wise-man.balthasar>.inner { clip-path: polygon(0 0, 100% 0, 100% 80%, 75% 100%, 25% 100%, 0 80%); } .magi>.wise-man.casper { grid-area: 4 / 2 / 6 / 4; +} + +.magi>.wise-man.casper, +.magi>.wise-man.casper>.inner { clip-path: polygon(0 0, 65% 0, 100% 44%, 100% 100%, 0 100%); +} + +@keyframes flicker-animation { + 0% {} + + 50% { + background: black; + color: black; + border-color: black; + } +} + +.flicker { + animation: flicker-animation 0.25s infinite step-end; +} + + +.connection { + height: 10px; + background: #ff8d00; + align-self: center; + margin: -10%; +} + +.connection.casper-balthasar { + grid-area: 4 / 3 / 5 / 4; + transform: rotate(-54deg); +} + +.connection.casper-melchior { + grid-area: 5 / 4 / 6 / 5; +} + +.connection.balthasar-melchior { + grid-area: 4 / 5 / 5 / 6; + transform: rotate(54deg); } \ No newline at end of file diff --git a/components/response.js b/components/response.js new file mode 100644 index 0000000..00fe641 --- /dev/null +++ b/components/response.js @@ -0,0 +1,45 @@ +import React from 'react'; + +function getStatusText(status) { + if (status === 'info') + return '情 報'; + + if (status === 'yes') + return '合 意'; + + if (status === 'no') + return '拒 絶'; + + if (status === 'conditional') + return '状 態'; + + throw new Error('Invalid status: ' + status); +} + +function getStatusColor(status) { + if (status === 'info') + return '#3caee0'; + + if (status === 'yes') + return '#52e691'; + + if (status === 'no') + return '#a41413'; + + if (status === 'conditional') + return '#ff8d00'; + + throw new Error('Invalid status: ' + status); +} + +export default function Response(props) { + const { status } = props; + + const text = getStatusText(status); + const color = getStatusColor(status); + + return React.createElement('div', { className: 'response', style: { color: color, borderColor: color } }, + [ + React.createElement('div', { className: 'inner' }, [text]) + ]); +} \ No newline at end of file diff --git a/components/wise_man.js b/components/wise_man.js index d8ce7f4..de58923 100644 --- a/components/wise_man.js +++ b/components/wise_man.js @@ -1,10 +1,7 @@ import React from 'react'; -function useColor(status, processing) { - if (processing) - return '#f1f1f1'; - +function useColor(status) { if (status === 'yes') return '#52e691'; @@ -14,16 +11,22 @@ function useColor(status, processing) { if (status === 'info') return '#3caee0'; + if (status == 'conditional') + return 'repeating-linear-gradient(56deg, rgb(82, 230, 145) 0px, rgb(82, 230, 145) 30px, #82cd68 30px, #82cd68 60px)'; + throw new Error(`Invalid status: ${status}`); } export default function WiseMan(props) { const { setProps, name, order_number, question_id, answer } = props; const fullName = `${name.toUpperCase()} • ${order_number}`; - const color = useColor(answer['status'], question_id !== answer['id']); + const color = useColor(answer['status']); + const processing = question_id !== answer['id']; - return React.createElement('div', { className: `wise-man ${name}`, style: { background: color } }, - [ - fullName - ]) + return React.createElement('div', { className: `wise-man ${name}` }, + [ + React.createElement('div', { className: `inner ${processing ? 'flicker' : ''}`, style: { background: color } }, [ + fullName + ]) + ]) } \ No newline at end of file diff --git a/main.py b/main.py index f394ff0..373fd09 100644 --- a/main.py +++ b/main.py @@ -1,28 +1,53 @@ from dataclasses import dataclass +import os from dash_extensions.enrich import Dash, Input, Output, State, Trigger, callback, ALL, MATCH import dash_core_components as dcc import dash_html_components as html from dash_local_react_components import load_react_component import random +import ai app = Dash(__name__) Magi = load_react_component(app, 'components', 'magi.js') WiseMan = load_react_component(app, 'components', 'wise_man.js') +Response = load_react_component(app, 'components', 'response.js') app.layout = html.Div(children=[ Magi(id='magi', children=[ + html.Div(className='connection casper-balthasar'), + html.Div(className='connection casper-melchior'), + html.Div(className='connection balthasar-melchior'), html.Div( className='system-status', children=[ - html.Div(children='CODE : 473'), - html.Div(children='FILE : MAGI_SYS'), - html.Div(id='extention', children='EXTENTION : ????'), - html.Div(children='EX_MODE : OFF'), - html.Div(children='PRIORITY : AAA')]), - WiseMan(id={'type': 'wise-man', 'name': 'melchior'}, name='melchior', order_number=1, question_id=0, answer={'id': 0, 'response': 'yes', 'status': 'yes'}), - WiseMan(id={'type': 'wise-man', 'name': 'balthasar'}, name='balthasar', order_number=2, question_id=0, answer={'id': 0, 'response': 'yes', 'status': 'yes'}), - WiseMan(id={'type': 'wise-man', 'name': 'casper'}, name='casper', order_number=3, question_id=0, answer={'id': 0, 'response': 'yes', 'status': 'yes'}), + html.Div(children='CODE:473'), + html.Div(children='FILE:MAGI_SYS'), + html.Div(id='extention', children='EXTENTION:????'), + html.Div(children='EX_MODE:OFF'), + html.Div(children='PRIORITY:AAA')]), + WiseMan( + id={'type': 'wise-man', 'name': 'melchior'}, + name='melchior', + order_number=1, + personality='You are a scientist. Your goal is to further our our understanding of the universe and advance our technological progress.', + question_id=0, + answer={'id': 0, 'response': 'yes', 'status': 'yes'}), + WiseMan( + id={'type': 'wise-man', 'name': 'balthasar'}, + name='balthasar', + order_number=2, + personality='You are a mother. Your goal is to protect your children and ensure their well-being.', + question_id=0, + answer={'id': 0, 'response': 'yes', 'status': 'yes'}), + WiseMan( + id={'type': 'wise-man', 'name': 'casper'}, + name='casper', + order_number=3, + personality='You are a woman. Your goal is to pursue your dreams and desires.', + question_id=0, + answer={'id': 0, 'response': 'yes', 'status': 'yes'}), + Response(id='response', status='info'), html.Div(className='title', children='MAGI'), html.Div(className='header left', children=[ html.Hr(), @@ -39,6 +64,8 @@ app.layout = html.Div(children=[ html.Hr() ]) ]), + html.Label('MAGI access codes: '), + dcc.Input(id='key', autoComplete='off', type='password', value=os.getenv('OPENAI_API_KEY', '')), dcc.Input(id='query', type='text', value='', debounce=True, autoComplete='off'), dcc.Store(id='question', data={'id': 0, 'query': ''}), @@ -51,16 +78,25 @@ app.layout = html.Div(children=[ @callback( Output('question', 'data'), Input('query', 'value'), - State('question', 'data')) + State('question', 'data'), + prevent_initial_call=True) def question(query: str, question: dict): + print('question') + return {'id': question['id'] + 1, 'query': query} @callback( Output('annotated-question', 'data'), - Input('question', 'data')) -def annotated_question(question: dict): - return {'id': question['id'], 'query': question['query'], 'is_yes_no_question': question['query'].endswith('?')} + Input('question', 'data'), + State('key', 'value'), + prevent_initial_call=True) +def annotated_question(question: dict, key: str): + print('annotated_question') + + is_yes_no_question = ai.is_yes_or_no_question(question['query'], key) + + return {'id': question['id'], 'query': question['query'], 'is_yes_no_question': is_yes_no_question} @callback( @@ -69,10 +105,10 @@ def annotated_question(question: dict): Input('annotated-question', 'data')) def extention(question: dict, annotated_question: dict): if question['id'] != annotated_question['id']: - return 'EXTENTION : ????' + return 'EXTENTION:????' code = '2137' if annotated_question['is_yes_no_question'] else '3023' - return f'EXTENTION : {code}' + return f'EXTENTION:{code}' @callback( @@ -90,5 +126,17 @@ def question_id(question: dict): return question['id'] +@callback( + Output('response', 'status'), + Input({'type': 'wise-man', 'name': ALL}, 'answer')) +def response_status(answers: list): + if all([answer['status'] == 'yes' for answer in answers]): + return 'yes' + elif any([answer['status'] == 'no' for answer in answers]): + return 'no' + else: + return 'info' + + if __name__ == '__main__': app.run_server(debug=True)