Skip to content

Commit

Permalink
feat: scaffold copilot
Browse files Browse the repository at this point in the history
  • Loading branch information
philippfromme committed Jul 7, 2023
1 parent 3d7fb25 commit aef424d
Show file tree
Hide file tree
Showing 11 changed files with 1,513 additions and 281 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ index.d.ts
lib/**/*.d.ts
.idea
*.iml
.DS_Store
.DS_Store
.env
4 changes: 4 additions & 0 deletions assets/bpmn-js.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,7 @@
height: 20px;
width: 20px;
}

.layer-copilot {
opacity: 0.5;
}
2 changes: 2 additions & 0 deletions lib/Modeler.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import BendpointsModule from 'diagram-js/lib/features/bendpoints';
import ConnectModule from 'diagram-js/lib/features/connect';
import ConnectionPreviewModule from 'diagram-js/lib/features/connection-preview';
import ContextPadModule from './features/context-pad';
import CopilotModule from './features/copilot';
import CopyPasteModule from './features/copy-paste';
import CreateModule from 'diagram-js/lib/features/create';
import DistributeElementsModule from './features/distribute-elements';
Expand Down Expand Up @@ -175,6 +176,7 @@ Modeler.prototype._modelingModules = [
ConnectModule,
ConnectionPreviewModule,
ContextPadModule,
CopilotModule,
CopyPasteModule,
CreateModule,
DistributeElementsModule,
Expand Down
138 changes: 138 additions & 0 deletions lib/features/copilot/Copilot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { clear as svgClear } from 'tiny-svg';

export default class Copilot {
constructor(
canvas,
createPreview,
directEditing,
eventBus,
keyboard,
modeling,
selection) {
this._canvas = canvas;
this._createPreview = createPreview;
this._directEditing = directEditing;
this._modeling = modeling;
this._selection = selection;

eventBus.on('selection.changed', (context) => {
const { newSelection } = context;

this.clearSuggestion();

if (newSelection.length === 1) {
this.makeSuggestion(newSelection[ 0 ]);
}
});

eventBus.on('directEditing.activate', () => {
this.hideSuggestion();
});

eventBus.on('directEditing.deactivate', () => {
this.showSuggestion();
});

keyboard.addListener((context) => {
if (context.keyEvent.key === 'Tab') {
context.keyEvent.preventDefault();

this.acceptSuggestion();
} else if (context.keyEvent.key === 'Escape') {
this.clearSuggestion();
}
});

this._suggestion = null;

this._layer = canvas.getLayer('copilot', 1000);
}

acceptSuggestion() {
console.log('acceptSuggestion');

if (!this._suggestion) {
return;
}

const {
elements,
position,
target
} = this._suggestion;

const createdElements = this._modeling.createElements(elements, position, target, {
position: 'absolute'
});

this.clearSuggestion();

if (createdElements.length === 1) {
this._selection.select(createdElements[ 0 ]);

this._directEditing.activate(createdElements[ 0 ]);
}
}

async makeSuggestion(element) {
console.log('makeSuggestion');

const suggestion = this._suggestion = await this.createSuggestion(element);

console.log('suggestion', suggestion);

if (!suggestion) {
return;
}

const { elements } = suggestion;

const group = this._createPreview.createPreview(elements);

this._layer.appendChild(group);
}

showSuggestion() {
console.log('showSuggestion');

this._canvas.showLayer('copilot');
}

hideSuggestion() {
console.log('hideSuggestion');

this._canvas.hideLayer('copilot');
}

clearSuggestion() {
console.log('clearSuggestion');

this._suggestion = null;

svgClear(this._layer);
}

createSuggestion(element) {
console.log('createSuggestion');

if (!this._provider) {
return null;
}

return this._provider.createSuggestion(element);
}

setProvider(provider) {
this._provider = provider;
}
}

Copilot.$inject = [
'canvas',
'createPreview',
'directEditing',
'eventBus',
'keyboard',
'modeling',
'selection'
];
194 changes: 194 additions & 0 deletions lib/features/copilot/OpenAIProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { OpenAI } from 'langchain/llms/openai';

import {
getBusinessObject,
is
} from '../../util/ModelUtil';

const model = new OpenAI({

// modelName: 'text-davinchi-003',
maxTokens: 2000,

// eslint-disable-next-line no-undef
openAIApiKey: process.env.OPENAI_API_KEY,
temperature: 0.9,
verbose: true,
});

export default class OpenAIProvider {
constructor(autoPlace, connectionPreview, copilot, elementFactory, eventBus, layouter, rules) {
this._autoPlace = autoPlace;
this._connectionPreview = connectionPreview;
this._eventBus = eventBus;
this._layouter = layouter;
this._rules = rules;

copilot.setProvider(this);

this._elementFactory = elementFactory;
}

async createSuggestion(element) {
if (!is(element, 'bpmn:FlowNode')) {
return null;
}

const suggestedNextElement = await suggestNextElement(element);

if (!suggestedNextElement) {
return null;
}

const {
name,
type
} = suggestedNextElement;

const nextElement = this._elementFactory.createShape({
type
});

const elements = [ nextElement ];

nextElement.businessObject.set('name', name);

console.log('nextElement', nextElement);

const position = this._eventBus.fire('autoPlace', {
source: element,
shape: nextElement
});

if (!position) {
throw new Error('no position');
}

nextElement.x = position.x - nextElement.width / 2;
nextElement.y = position.y - nextElement.height / 2;

const allowed = this._rules.allowed('connection.create', {
source: element,
target: nextElement,
hints: {
targetParent: element.parent
}
});

if (allowed) {
const connection = this._elementFactory.createConnection({
...allowed,
source: element,
target: nextElement
});

connection.waypoints = this._layouter.layoutConnection(connection, {
source: element,
target: nextElement
});

elements.push(connection);
}

console.log('elements', elements);

return {
elements,
position: null,
target: element.parent
};
}
}

OpenAIProvider.$inject = [
'autoPlace',
'connectionPreview',
'copilot',
'elementFactory',
'eventBus',
'layouter',
'rules'
];

async function suggestNextElement(element) {
const prompt = [
'BPMN Process:',
...getEdges(element),
'Selected element:',
getName(element),
'Suggested next element:'
].join('\n');

console.log('prompt', prompt);

const response = await model.call(prompt);

console.log('response', response);

try {
const [ _, name, type ] = /(.*)\[(.*)\]/.exec(response);

console.log('name', name);
console.log('type', type);

return {
type,
name
};
} catch (error) {
return null;
}
}

function getEdges(element, edges = [], visitedSequenceFlows = []) {
element = getBusinessObject(element);

const incoming = element.get('incoming');

if (!incoming) {
return edges;
}

incoming
.filter(
(incoming) =>
is(incoming, 'bpmn:SequenceFlow') &&
!visitedSequenceFlows.includes(incoming)
)
.forEach((sequenceFlow) => {
visitedSequenceFlows.push(sequenceFlow);

const edge = getEdge(sequenceFlow);

edges.push(edge, ...getEdges(sequenceFlow.get('sourceRef')));
});

return edges;
}

function getEdge(sequenceFlow) {
const name = getName(sequenceFlow, true);

const source = sequenceFlow.get('sourceRef'),
target = sequenceFlow.get('targetRef');

return name
? `${getName(source)}[${source.$type}] -- ${name} --> ${getName(target)}[${
target.$type
}]`
: `${getName(source)}[${source.$type}] --> ${getName(target)}[${
target.$type
}]`;
}

function getName(element, strict = false) {
const businessObject = getBusinessObject(element);

let name = businessObject.get('name');

if (!name && !strict) {
name = businessObject.get('id');
}

return name;
}
15 changes: 15 additions & 0 deletions lib/features/copilot/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Copilot from './Copilot';
import OpenAIProvider from './OpenAIProvider';

export default {
__depends__: [

// TODO
],
__init__: [
'copilot',
'openaiprovider'
],
copilot: [ 'type', Copilot ],
openaiprovider: [ 'type', OpenAIProvider ]
};
Loading

0 comments on commit aef424d

Please sign in to comment.