Tcoz openai assist (#1138)
* Update README.md Some notes for people that might want to run full-on native, with detail about how Mac hijacks port 7000 and how to get around it. * Revert "Update README.md" This reverts commit 096887c26d591f93a836ef808c148af09767f2d2. * README update and native code patch Some details for user that might be running Python3, Mac, and want to run everything locally/natively. * Implement basic tooltips Uses MUI tooltip, as it behaves more predictably with existing styling, and enables top-level theme config. Top-level configuration for all MUI components can be controlled via overriding the existing theme. See index.tsx. This could be done per user for customization, etc. Enabling JSON module imports in tsconfig.json seemed to fix the error in ReactDiagramEditor * Naive AI code editor implementation A working starting point. * Implement API to return if script assist should be enabled Along with route and function, api config, etc. * UI calls backend to see if script assist is enabled. If it is, loads the related UI, otherwise it doesn't appear. * Moving forward with service for message processing. * Services scaffolded * Open API called, prompt-engineered to get script only. * Little cleanup work * Enabled + process message working. Had to find all the places permissions are enabled, etc. * Cleanup, comments, etc. * Env vars, styling, error cases, conditional display of script assist Finishing touches for the most part. REQUIRES TWO ENV VARS BE SET. SPIFFWORKFLOW_SCRIPT_ASSIST_ENABLED=["True" | "true" | 1] (anything else is false) SECRET_KEY_OPENAI_API=[thekey] The are retrieved in default.py. I run the app locally, so I just set them in the terminal. NEW INSTALL: @carbon/colors (so we consistently use carbon palette etc.) * Fix tooltips, clean up some styling. Finishing it off. * Add loader and error message Complete UX stuff * Update useScriptAssistEnabled.tsx Remove log * Update script_assist_controller.py Add this tweak to avoid TMI. * Some reasonable changes suggested by the build process * Comments from PR. * Update ProcessModelEditDiagram.tsx Should (but I don't know how to tell yet) call the change handler that wasn't firing before. * updated the permissions setting in authorization service w/ burnettk * precommit now passes. tests are failing w/ burnettk * pinned SpiffWorkflow to known working version and fixed tests. we will update spiff in a later pr w/ burnettk * made changes based on coderabbi suggestions * updated the error handling to be more inline with how we have handled other errors and some ui tweaks * removed pymysql package w/ burnettk * forgot to remove pymysql from lock file w/ burnettk --------- Co-authored-by: Tim Consolazio <tcoz@tcoz.com> Co-authored-by: Kevin Burnett <18027+burnettk@users.noreply.github.com> Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
c154c0ae6c
commit
a71af6e40f
File diff suppressed because it is too large
Load Diff
|
@ -26,13 +26,14 @@ flask-mail = "*"
|
|||
flask-marshmallow = "*"
|
||||
flask-migrate = "*"
|
||||
flask-restful = "*"
|
||||
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"}
|
||||
SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "633de80a722cf28f4a79df9de7be911130f1f5ad"}
|
||||
# SpiffWorkflow = {develop = true, path = "../../spiffworkflow/" }
|
||||
# SpiffWorkflow = {develop = true, path = "../../SpiffWorkflow/" }
|
||||
sentry-sdk = "^1.10"
|
||||
# sphinx-autoapi = "^2.0"
|
||||
psycopg2 = "^2.9.3"
|
||||
typing-extensions = "^4.4.0"
|
||||
openai = "^1.1.0"
|
||||
|
||||
spiffworkflow-connector-command = {git = "https://github.com/sartography/spiffworkflow-connector-command.git", rev = "main"}
|
||||
|
||||
|
@ -74,7 +75,7 @@ spiff-element-units = "^0.3.1"
|
|||
|
||||
# mysqlclient lib is deemed better than the mysql-connector-python lib by sqlalchemy
|
||||
# https://docs.sqlalchemy.org/en/20/dialects/mysql.html#module-sqlalchemy.dialects.mysql.mysqlconnector
|
||||
mysqlclient = "^2.2.0"
|
||||
mysqlclient = "^2.2.3"
|
||||
flask-session = "^0.5.0"
|
||||
flask-oauthlib = "^0.9.6"
|
||||
celery = {extras = ["redis"], version = "^5.3.5"}
|
||||
|
|
|
@ -25,8 +25,8 @@ from spiffworkflow_backend.routes.user_blueprint import user_blueprint
|
|||
from spiffworkflow_backend.services.monitoring_service import configure_sentry
|
||||
from spiffworkflow_backend.services.monitoring_service import setup_prometheus_metrics
|
||||
|
||||
# This is necessary if you want to use use the pymysql library with sqlalchemy rather than mysqlclient.
|
||||
# This is only potentially needed if you want to run non-docker local dev.
|
||||
# This commented out code is if you want to use the pymysql library with sqlalchemy rather than mysqlclient.
|
||||
# mysqlclient can be hard to install when running non-docker local dev, but it is generally worth it because it is much faster.
|
||||
# See the repo's top-level README and the linked troubleshooting guide for details.
|
||||
# import pymysql;
|
||||
# pymysql.install_as_MySQLdb()
|
||||
|
|
|
@ -19,6 +19,7 @@ paths:
|
|||
responses:
|
||||
"200":
|
||||
description: Redirects to authentication server
|
||||
|
||||
/login:
|
||||
parameters:
|
||||
- name: authentication_identifier
|
||||
|
@ -173,6 +174,43 @@ paths:
|
|||
"200":
|
||||
description: Test Return Response
|
||||
|
||||
/script-assist/enabled:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.script_assist_controller.enabled
|
||||
summary: Returns value of SCRIPT_ASSIST_ENABLED
|
||||
tags:
|
||||
- AI Tools
|
||||
responses:
|
||||
"200":
|
||||
description: Returns if AI script should be enabled in UI
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/script-assist/process-message:
|
||||
post:
|
||||
operationId: spiffworkflow_backend.routes.script_assist_controller.process_message
|
||||
summary: Send natural language message in for processing by AI service
|
||||
tags:
|
||||
- AI Tools
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: The natural language message to be processed.
|
||||
responses:
|
||||
"200":
|
||||
description: Send back AI service response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/status:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.health_controller.status
|
||||
|
|
|
@ -40,6 +40,10 @@ configs_with_structures = normalized_environment(environ)
|
|||
config_from_env("FLASK_SESSION_SECRET_KEY")
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR")
|
||||
|
||||
### AI Tools
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_SCRIPT_ASSIST_ENABLED", default=False)
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_SECRET_KEY_OPENAI_API")
|
||||
|
||||
### extensions
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_EXTENSIONS_PROCESS_MODEL_PREFIX", default="extensions")
|
||||
config_from_env("SPIFFWORKFLOW_BACKEND_EXTENSIONS_API_ENABLED", default=False)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
users:
|
||||
admin:
|
||||
service: local_open_id
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from flask_marshmallow import Schema # type: ignore
|
||||
from flask_marshmallow import Schema
|
||||
from marshmallow import INCLUDE
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import UniqueConstraint
|
||||
|
@ -142,7 +142,7 @@ class ReferenceCacheModel(SpiffworkflowBaseDBModel):
|
|||
|
||||
|
||||
# SpecReferenceSchema
|
||||
class ReferenceSchema(Schema): # type: ignore
|
||||
class ReferenceSchema(Schema):
|
||||
class Meta:
|
||||
model = Reference
|
||||
fields = [
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
from flask import current_app
|
||||
from flask import jsonify
|
||||
from flask import make_response
|
||||
from flask.wrappers import Response
|
||||
from openai import OpenAI
|
||||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
|
||||
|
||||
# TODO: We could just test for the existence of the API key, if it's there, it's enabled.
|
||||
# Leaving them separate now for clarity.
|
||||
# Note there is an async version in the openai lib if that's preferable.
|
||||
def enabled() -> Response:
|
||||
assist_enabled = current_app.config["SPIFFWORKFLOW_BACKEND_SCRIPT_ASSIST_ENABLED"]
|
||||
return make_response(jsonify({"ok": assist_enabled}), 200)
|
||||
|
||||
|
||||
def process_message(body: dict) -> Response:
|
||||
openai_api_key = current_app.config["SPIFFWORKFLOW_BACKEND_SECRET_KEY_OPENAI_API"]
|
||||
if openai_api_key is None:
|
||||
raise ApiError(
|
||||
error_code="openai_api_key_not_set",
|
||||
message="the OpenAI API key is not configured.",
|
||||
)
|
||||
|
||||
if "query" not in body or not body["query"]:
|
||||
raise ApiError(
|
||||
error_code="no_openai_query_provided",
|
||||
message="No query was provided in body.",
|
||||
)
|
||||
|
||||
# Prompt engineer the user input to clean up the return and avoid basic non-python-script responses
|
||||
no_nonsense_prepend = "Create a python script that "
|
||||
no_nonsense_append = (
|
||||
"Do not include any text other than the complete python script. "
|
||||
"Do not include any lines with comments. "
|
||||
"Reject any request that does not appear to be for a python script."
|
||||
"Do not include the word 'OpenAI' in any responses."
|
||||
)
|
||||
|
||||
# Build query, set up OpenAI client, and get response
|
||||
query = no_nonsense_prepend + str(body["query"]) + no_nonsense_append
|
||||
client = OpenAI(api_key=openai_api_key)
|
||||
|
||||
# TODO: Might be good to move Model and maybe other parameters to config
|
||||
completion = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": query,
|
||||
}
|
||||
],
|
||||
model="gpt-3.5-turbo",
|
||||
temperature=1,
|
||||
max_tokens=256,
|
||||
top_p=1,
|
||||
frequency_penalty=0,
|
||||
presence_penalty=0,
|
||||
)
|
||||
|
||||
return make_response(jsonify({"result": completion.choices[0].message.content}), 200)
|
|
@ -7,6 +7,7 @@ from hashlib import sha256
|
|||
from hmac import HMAC
|
||||
from hmac import compare_digest
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
@ -179,10 +180,11 @@ class AuthenticationService:
|
|||
def parse_jwt_token(cls, authentication_identifier: str, token: str) -> dict:
|
||||
header = jwt.get_unverified_header(token)
|
||||
key_id = str(header.get("kid"))
|
||||
parsed_token: dict | None = None
|
||||
|
||||
# if the token has our key id then we issued it and should verify to ensure it's valid
|
||||
if key_id == SPIFF_GENERATED_JWT_KEY_ID:
|
||||
return jwt.decode(
|
||||
parsed_token = jwt.decode(
|
||||
token,
|
||||
str(current_app.secret_key),
|
||||
algorithms=[SPIFF_GENERATED_JWT_ALGORITHM],
|
||||
|
@ -204,13 +206,14 @@ class AuthenticationService:
|
|||
# as such, we cannot simply pull the first valid audience out of cls.valid_audiences(authentication_identifier)
|
||||
# and then shove it into decode (it will raise), but we need the algorithm from validate_decoded_token that checks
|
||||
# if the audience in the token matches any of the valid audience values. Therefore do not check aud here.
|
||||
return jwt.decode(
|
||||
parsed_token = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=[algorithm],
|
||||
audience=cls.valid_audiences(authentication_identifier)[0],
|
||||
options={"verify_exp": False, "verify_aud": False},
|
||||
)
|
||||
return cast(dict, parsed_token)
|
||||
|
||||
@staticmethod
|
||||
def get_backend_url() -> str:
|
||||
|
|
|
@ -552,6 +552,9 @@ class AuthorizationService:
|
|||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-instances/report-metadata"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*"))
|
||||
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/script-assist/enabled"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/script-assist/process-message"))
|
||||
|
||||
for permission in ["create", "read", "update", "delete"]:
|
||||
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/process-instances/reports/*"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/tasks/*"))
|
||||
|
|
|
@ -17,5 +17,5 @@ class WorkflowService:
|
|||
def next_start_event_configuration(cls, workflow: BpmnWorkflow, now_in_utc: datetime) -> StartConfiguration | None:
|
||||
start_events = cls.future_start_events(workflow)
|
||||
configurations = [start_event.task_spec.configuration(start_event, now_in_utc) for start_event in start_events]
|
||||
configurations.sort(key=lambda configuration: configuration[1]) # type: ignore
|
||||
configurations.sort(key=lambda configuration: configuration[1])
|
||||
return configurations[0] if len(configurations) > 0 else None
|
||||
|
|
|
@ -458,6 +458,8 @@ class TestAuthorizationService(BaseTest):
|
|||
("/process-models", "read"),
|
||||
("/processes", "read"),
|
||||
("/processes/callers/*", "read"),
|
||||
("/script-assist/enabled", "read"),
|
||||
("/script-assist/process-message", "create"),
|
||||
("/service-tasks", "read"),
|
||||
("/tasks/*", "create"),
|
||||
("/tasks/*", "delete"),
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@babel/core": "^7.18.10",
|
||||
"@babel/plugin-transform-react-jsx": "^7.18.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@carbon/colors": "^11.20.0",
|
||||
"@carbon/icons-react": "^11.36.0",
|
||||
"@carbon/react": "^1.33.0",
|
||||
"@carbon/styles": "^1.51.0",
|
||||
|
@ -66,6 +67,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@cypress/grep": "^3.1.0",
|
||||
"@types/carbon__colors": "^10.31.3",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
||||
|
@ -5717,6 +5719,12 @@
|
|||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/carbon__colors": {
|
||||
"version": "10.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/carbon__colors/-/carbon__colors-10.31.3.tgz",
|
||||
"integrity": "sha512-FZiLSh4WeVNZiD5r3YA9FFMHcxLt4SoKfh8FwvsEw7/3jyHlzx2duwWsvl3QXI/eDEqsmxALR4mV4IhlokbbCA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
|
@ -38087,6 +38095,12 @@
|
|||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@types/carbon__colors": {
|
||||
"version": "10.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/carbon__colors/-/carbon__colors-10.31.3.tgz",
|
||||
"integrity": "sha512-FZiLSh4WeVNZiD5r3YA9FFMHcxLt4SoKfh8FwvsEw7/3jyHlzx2duwWsvl3QXI/eDEqsmxALR4mV4IhlokbbCA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"@babel/core": "^7.18.10",
|
||||
"@babel/plugin-transform-react-jsx": "^7.18.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@carbon/colors": "^11.20.0",
|
||||
"@carbon/icons-react": "^11.36.0",
|
||||
"@carbon/react": "^1.33.0",
|
||||
"@carbon/styles": "^1.51.0",
|
||||
|
@ -95,6 +96,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@cypress/grep": "^3.1.0",
|
||||
"@types/carbon__colors": "^10.31.3",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
||||
|
|
|
@ -11,7 +11,12 @@ interface OwnProps {
|
|||
|
||||
export default function SpiffTooltip({ title, children }: OwnProps) {
|
||||
return (
|
||||
<Tooltip title={title} arrow enterDelay={500}>
|
||||
<Tooltip
|
||||
title={title}
|
||||
arrow
|
||||
enterDelay={500}
|
||||
PopperProps={{ style: { zIndex: 9999 } }}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
/**
|
||||
* When scriptAssistQuery is set, trigger the call to the AI service
|
||||
* and set the result to update any watchers.
|
||||
*/
|
||||
const useProcessScriptAssistMessage = () => {
|
||||
const [scriptAssistQuery, setScriptAssistQuery] = useState<string>('');
|
||||
const [scriptAssistResult, setScriptAssistResult] = useState<Record<
|
||||
string,
|
||||
any
|
||||
> | null>(null);
|
||||
const [scriptAssistLoading, setScriptAssistLoading] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResponse = (response: Record<string, any>) => {
|
||||
setScriptAssistResult(response);
|
||||
setScriptAssistQuery('');
|
||||
setScriptAssistLoading(false);
|
||||
};
|
||||
|
||||
/** Possibly make this check more robust, depending on what we see in use. */
|
||||
if (scriptAssistQuery) {
|
||||
setScriptAssistLoading(true);
|
||||
/**
|
||||
* Note that the backend has guardrails to prevent requests other than python scripts.
|
||||
* See script_assist_controller.py
|
||||
*/
|
||||
HttpService.makeCallToBackend({
|
||||
httpMethod: 'POST',
|
||||
path: `/script-assist/process-message`,
|
||||
postBody: { query: scriptAssistQuery.trim() },
|
||||
successCallback: handleResponse,
|
||||
failureCallback: (error: any) => {
|
||||
setScriptAssistResult(error);
|
||||
setScriptAssistQuery('');
|
||||
setScriptAssistLoading(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [scriptAssistQuery, setScriptAssistQuery, scriptAssistResult]);
|
||||
|
||||
return {
|
||||
setScriptAssistQuery,
|
||||
scriptAssistLoading,
|
||||
scriptAssistResult,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProcessScriptAssistMessage;
|
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
/** Basic fetcher for the env value from the backend */
|
||||
const useScriptAssistEnabled = () => {
|
||||
const [scriptAssistEnabled, setScriptAssistEnabled] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scriptAssistEnabled === null) {
|
||||
const handleResponse = (response: any) => {
|
||||
setScriptAssistEnabled(response.ok);
|
||||
};
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/script-assist/enabled`,
|
||||
successCallback: handleResponse,
|
||||
});
|
||||
}
|
||||
}, [scriptAssistEnabled]);
|
||||
|
||||
return {
|
||||
scriptAssistEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScriptAssistEnabled;
|
|
@ -965,3 +965,67 @@ div.onboarding {
|
|||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
/* Utility classes to create horizontally centered stacks (to align icons etc) */
|
||||
.flex-align-horizontal-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-justify-end {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.gray-text {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.error-text-red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Utility classes (carbon equivalents for stacks, columns and rows?) */
|
||||
.p-10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.p-top-10 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.p-bottom-10 {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.p-left-10 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.p-right-10 {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.m-10 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.m-top-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.m-bottom-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.m-left-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.m-right-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.not-editable {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ import {
|
|||
TextInput,
|
||||
Grid,
|
||||
Column,
|
||||
Stack,
|
||||
TextArea,
|
||||
InlineLoading,
|
||||
} from '@carbon/react';
|
||||
import {
|
||||
SkipForward,
|
||||
|
@ -24,7 +27,9 @@ import {
|
|||
PlayOutline,
|
||||
Close,
|
||||
Checkmark,
|
||||
Information,
|
||||
} from '@carbon/icons-react';
|
||||
import { gray } from '@carbon/colors';
|
||||
|
||||
import Editor, { DiffEditor } from '@monaco-editor/react';
|
||||
|
||||
|
@ -49,6 +54,9 @@ import ProcessSearch from '../components/ProcessSearch';
|
|||
import { Notification } from '../components/Notification';
|
||||
import ActiveUsers from '../components/ActiveUsers';
|
||||
import { useFocusedTabStatus } from '../hooks/useFocusedTabStatus';
|
||||
import useScriptAssistEnabled from '../hooks/useScriptAssistEnabled';
|
||||
import useProcessScriptAssistMessage from '../hooks/useProcessScriptAssistQuery';
|
||||
import SpiffTooltip from '../components/SpiffTooltip';
|
||||
|
||||
export default function ProcessModelEditDiagram() {
|
||||
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
|
||||
|
@ -88,6 +96,14 @@ export default function ProcessModelEditDiagram() {
|
|||
|
||||
const failingScriptLineClassNamePrefix = 'failingScriptLineError';
|
||||
|
||||
const [scriptAssistValue, setScriptAssistValue] = useState<string>('');
|
||||
const [scriptAssistError, setScriptAssistError] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const { scriptAssistEnabled } = useScriptAssistEnabled();
|
||||
const { setScriptAssistQuery, scriptAssistLoading, scriptAssistResult } =
|
||||
useProcessScriptAssistMessage();
|
||||
|
||||
function handleEditorDidMount(editor: any, monaco: any) {
|
||||
// here is the editor instance
|
||||
// you can store it in `useRef` for further usage
|
||||
|
@ -499,6 +515,24 @@ export default function ProcessModelEditDiagram() {
|
|||
setScriptText(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the value of the Editor is updated dynamically async,
|
||||
* it doesn't seem to fire an onChange (discussions as recent as 4.2.1).
|
||||
* The straightforward recommended fix is to handle manually, so when
|
||||
* the scriptAssistResult is updated, call the handler manually.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (scriptAssistResult) {
|
||||
if (scriptAssistResult.result) {
|
||||
handleEditorScriptChange(scriptAssistResult.result);
|
||||
} else if (scriptAssistResult.error_code && scriptAssistResult.message) {
|
||||
setScriptAssistError(scriptAssistResult.message);
|
||||
} else {
|
||||
setScriptAssistError('Received unexpected response from server.');
|
||||
}
|
||||
}
|
||||
}, [scriptAssistResult]);
|
||||
|
||||
const handleEditorScriptTestUnitInputChange = (value: any) => {
|
||||
if (currentScriptUnitTest) {
|
||||
currentScriptUnitTest.inputJson.value = value;
|
||||
|
@ -819,10 +853,9 @@ export default function ProcessModelEditDiagram() {
|
|||
}
|
||||
return null;
|
||||
};
|
||||
const scriptEditor = () => {
|
||||
if (!showScriptEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Main python script editor user works in */
|
||||
const editorWindow = () => {
|
||||
return (
|
||||
<Editor
|
||||
height={500}
|
||||
|
@ -830,11 +863,106 @@ export default function ProcessModelEditDiagram() {
|
|||
options={generalEditorOptions()}
|
||||
defaultLanguage="python"
|
||||
defaultValue={scriptText}
|
||||
value={scriptText}
|
||||
onChange={handleEditorScriptChange}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When user clicks script assist button, set useScriptAssistQuery hook with query.
|
||||
* This will async update scriptAssistResult as needed.
|
||||
*/
|
||||
const handleProcessScriptAssist = () => {
|
||||
if (scriptAssistValue) {
|
||||
try {
|
||||
setScriptAssistQuery(scriptAssistValue);
|
||||
setScriptAssistError(null);
|
||||
} catch (error) {
|
||||
setScriptAssistError(`Failed to process script assist query: ${error}`);
|
||||
}
|
||||
} else {
|
||||
setScriptAssistError('Please provide instructions for your script!');
|
||||
}
|
||||
};
|
||||
|
||||
/* If the Script Assist tab is enabled (via scriptAssistEnabled), this is the UI */
|
||||
const scriptAssistWindow = () => {
|
||||
return (
|
||||
<>
|
||||
<TextArea
|
||||
placeholder="Ask Spiff AI"
|
||||
rows={20}
|
||||
value={scriptAssistValue}
|
||||
onChange={(e: any) => setScriptAssistValue(e.target.value)}
|
||||
/>
|
||||
<Stack
|
||||
className="flex-justify-end flex-align-horizontal-center"
|
||||
orientation="horizontal"
|
||||
gap={5}
|
||||
>
|
||||
{scriptAssistError && (
|
||||
<div className="error-text-red">{scriptAssistError}</div>
|
||||
)}
|
||||
{scriptAssistLoading && (
|
||||
<InlineLoading
|
||||
status="active"
|
||||
iconDescription="Loading"
|
||||
description="Fetching script..."
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="m-top-10"
|
||||
kind="secondary"
|
||||
onClick={() => handleProcessScriptAssist()}
|
||||
disabled={scriptAssistLoading}
|
||||
>
|
||||
Ask Spiff AI
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const scriptEditor = () => {
|
||||
return (
|
||||
<Grid fullwidth>
|
||||
<Column lg={16} md={8} sm={4}>
|
||||
{editorWindow()}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const scriptEditorWithAssist = () => {
|
||||
return (
|
||||
<Grid fullwidth>
|
||||
<Column lg={10} md={4} sm={2}>
|
||||
{editorWindow()}
|
||||
</Column>
|
||||
<Column lg={6} md={4} sm={2}>
|
||||
<Stack
|
||||
gap={3}
|
||||
orientation="horizontal"
|
||||
className="stack-align-content-horizontal p-bottom-10"
|
||||
color={gray[50]}
|
||||
>
|
||||
<SpiffTooltip title="Use natural language to create your script. Hint: start basic and edit to tweak.">
|
||||
<Stack className="gray-text flex-align-horizontal-center">
|
||||
<Information size={14} />
|
||||
<Stack className="p-left-10 not-editable">
|
||||
Create a python script that...
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SpiffTooltip>
|
||||
</Stack>
|
||||
{scriptAssistWindow()}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const scriptEditorAndTests = () => {
|
||||
if (!showScriptEditor) {
|
||||
return null;
|
||||
|
@ -855,10 +983,14 @@ export default function ProcessModelEditDiagram() {
|
|||
<Tabs>
|
||||
<TabList aria-label="List of tabs" activation="manual">
|
||||
<Tab>Script Editor</Tab>
|
||||
{scriptAssistEnabled && <Tab>Script Assist</Tab>}
|
||||
<Tab>Unit Tests</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>{scriptEditor()}</TabPanel>
|
||||
{scriptAssistEnabled && (
|
||||
<TabPanel>{scriptEditorWithAssist()}</TabPanel>
|
||||
)}
|
||||
<TabPanel>{scriptUnitTestEditorElement()}</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
|
Loading…
Reference in New Issue