-
Notifications
You must be signed in to change notification settings - Fork 4
OpenConceptLab/ocl_online#81 | Using AI Assistant to generate change comment in concept #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
70893e6
318a4df
b313d35
9c33fae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,22 +1,42 @@ | ||||||||||||||
| /*eslint no-process-env: 0*/ | ||||||||||||||
| /*global process*/ | ||||||||||||||
| import React from 'react'; | ||||||||||||||
| import { compact, map, isEmpty } from 'lodash'; | ||||||||||||||
| import { compact, map, isEmpty, flatten, values, keys, get, isArray, cloneDeep, isEqual, omit } from 'lodash'; | ||||||||||||||
| import TextField from '@mui/material/TextField' | ||||||||||||||
| import { flatten, values, keys, get, isArray } from 'lodash' | ||||||||||||||
| import IconButton from '@mui/material/IconButton' | ||||||||||||||
| import Tooltip from '@mui/material/Tooltip' | ||||||||||||||
| import CircularProgress from '@mui/material/CircularProgress' | ||||||||||||||
| import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; | ||||||||||||||
| import CloseIconButton from '../common/CloseIconButton'; | ||||||||||||||
| import APIService from '../../services/APIService' | ||||||||||||||
| import FormComponent, { CardSection } from '../common/FormComponent' | ||||||||||||||
| import { sortValuesBySourceSummary } from '../repos/utils'; | ||||||||||||||
| import { | ||||||||||||||
| fetchDatatypes, fetchNameTypes, fetchDescriptionTypes, fetchConceptClasses, fetchLocales | ||||||||||||||
| } from './utils'; | ||||||||||||||
| import { toParentURI } from '../../common/utils' | ||||||||||||||
| import { toParentURI, isSuperuser, hasAuthGroup, getCurrentUser } from '../../common/utils' | ||||||||||||||
| import { OperationsContext } from '../app/LayoutContext'; | ||||||||||||||
| import Button from '../common/Button' | ||||||||||||||
| import AutocompleteGroupByRepoSummary from '../common/AutocompleteGroupByRepoSummary' | ||||||||||||||
| import LocaleForm from './LocaleForm' | ||||||||||||||
| import Breadcrumbs from '../common/Breadcrumbs' | ||||||||||||||
| import CustomAttributesForm from '../common/CustomAttributesForm' | ||||||||||||||
|
|
||||||||||||||
| const TOP_LEVEL_PROMPT_EXCLUSIONS = [ | ||||||||||||||
| 'uuid', 'type', 'url', 'version', 'version_url', 'versions_url', 'versioned_object_id', 'created_on', | ||||||||||||||
| 'updated_on', 'created_by', 'updated_by', 'update_comment', 'comment', 'checksums', 'public_can_view', | ||||||||||||||
| 'latest_source_version', 'owner_url', 'owner_type' | ||||||||||||||
| ]; | ||||||||||||||
|
|
||||||||||||||
| const NAME_PROMPT_EXCLUSIONS = ['uuid', 'checksum', 'type']; | ||||||||||||||
| const DESCRIPTION_PROMPT_EXCLUSIONS = ['uuid', 'checksum', 'type']; | ||||||||||||||
| const MAPPING_PROMPT_EXCLUSIONS = [ | ||||||||||||||
| 'uuid', 'checksums', 'type', 'url', 'version', 'version_url', 'versioned_object_id', 'versioned_object_url', | ||||||||||||||
| 'created_on', 'updated_on', 'created_by', 'updated_by', 'update_comment', 'is_latest_version', | ||||||||||||||
| 'version_created_on', 'version_updated_on', 'version_updated_by', 'public_can_view', 'latest_source_version', | ||||||||||||||
| 'sort_weight', 'owner_type', 'from_source_owner_type', 'to_source_owner_type', 'from_source_version', | ||||||||||||||
| 'to_source_version', 'from_concept_url', 'from_source_url', 'from_source_owner', 'to_source_url', 'to_source_owner' | ||||||||||||||
| ]; | ||||||||||||||
|
|
||||||||||||||
| class ConceptForm extends FormComponent { | ||||||||||||||
| static contextType = OperationsContext; | ||||||||||||||
|
|
@@ -37,6 +57,7 @@ class ConceptForm extends FormComponent { | |||||||||||||
| selected_datatype: null, | ||||||||||||||
| manualMnemonic: false, | ||||||||||||||
| manualExternalId: false, | ||||||||||||||
| generatingChangeComment: false, | ||||||||||||||
| fields: { | ||||||||||||||
| id: {...mandatoryFieldStruct}, | ||||||||||||||
| concept_class: {...mandatoryFieldStruct}, | ||||||||||||||
|
|
@@ -54,6 +75,142 @@ class ConceptForm extends FormComponent { | |||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /*eslint no-undef: 0*/ | ||||||||||||||
| getAIAssistantURL = () => window.AI_ASSISTANT_API_URL || process.env.AI_ASSISTANT_API_URL | ||||||||||||||
|
Comment on lines
+78
to
+79
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the bare
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| sanitizeNameForPrompt = name => omit(name || {}, NAME_PROMPT_EXCLUSIONS) | ||||||||||||||
|
|
||||||||||||||
| sanitizeDescriptionForPrompt = description => omit(description || {}, DESCRIPTION_PROMPT_EXCLUSIONS) | ||||||||||||||
|
|
||||||||||||||
| sanitizeMappingForPrompt = mapping => omit(mapping || {}, MAPPING_PROMPT_EXCLUSIONS) | ||||||||||||||
|
|
||||||||||||||
| sanitizeConceptForPrompt = concept => { | ||||||||||||||
| const sanitized = omit(cloneDeep(concept || {}), TOP_LEVEL_PROMPT_EXCLUSIONS) | ||||||||||||||
|
|
||||||||||||||
| if (isArray(sanitized.names)) | ||||||||||||||
| sanitized.names = sanitized.names.map(this.sanitizeNameForPrompt) | ||||||||||||||
|
|
||||||||||||||
| if (isArray(sanitized.descriptions)) | ||||||||||||||
| sanitized.descriptions = sanitized.descriptions.map(this.sanitizeDescriptionForPrompt) | ||||||||||||||
|
|
||||||||||||||
| if (isArray(sanitized.mappings)) | ||||||||||||||
| sanitized.mappings = sanitized.mappings.map(this.sanitizeMappingForPrompt) | ||||||||||||||
|
|
||||||||||||||
| return sanitized | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| normalizeNamesForComparison = names => (names || []).map(name => ({ | ||||||||||||||
| locale: name?.locale || '', | ||||||||||||||
| name_type: name?.name_type || '', | ||||||||||||||
| locale_preferred: Boolean(name?.locale_preferred), | ||||||||||||||
| name: name?.name || '', | ||||||||||||||
| external_id: name?.external_id || '', | ||||||||||||||
| })) | ||||||||||||||
|
|
||||||||||||||
| normalizeDescriptionsForComparison = descriptions => (descriptions || []).map(description => ({ | ||||||||||||||
| locale: description?.locale || '', | ||||||||||||||
| description_type: description?.description_type || '', | ||||||||||||||
| locale_preferred: Boolean(description?.locale_preferred), | ||||||||||||||
| description: description?.description || '', | ||||||||||||||
| external_id: description?.external_id || '', | ||||||||||||||
| })) | ||||||||||||||
|
|
||||||||||||||
| getComparableOriginalConcept = () => { | ||||||||||||||
| const concept = this.props.concept || {} | ||||||||||||||
|
|
||||||||||||||
| return { | ||||||||||||||
| id: concept.id || '', | ||||||||||||||
| concept_class: concept.concept_class || '', | ||||||||||||||
| datatype: concept.datatype || '', | ||||||||||||||
| external_id: concept.external_id || '', | ||||||||||||||
| extras: concept.extras || {}, | ||||||||||||||
| parent_concept_urls: concept.parent_concept_urls || [], | ||||||||||||||
| names: this.normalizeNamesForComparison(concept.names), | ||||||||||||||
| descriptions: this.normalizeDescriptionsForComparison(concept.descriptions), | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| getComparableCurrentConcept = () => { | ||||||||||||||
| const valuesMap = this.getValues() | ||||||||||||||
|
|
||||||||||||||
| return { | ||||||||||||||
| id: valuesMap.id || '', | ||||||||||||||
| concept_class: valuesMap.concept_class || '', | ||||||||||||||
| datatype: valuesMap.datatype || '', | ||||||||||||||
| external_id: valuesMap.external_id || '', | ||||||||||||||
| extras: valuesMap.extras || {}, | ||||||||||||||
| parent_concept_urls: valuesMap.parent_concept_urls || [], | ||||||||||||||
| names: this.normalizeNamesForComparison(valuesMap.names), | ||||||||||||||
| descriptions: this.normalizeDescriptionsForComparison(valuesMap.descriptions), | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| hasConceptChanges = () => !isEqual(this.getComparableOriginalConcept(), this.getComparableCurrentConcept()) | ||||||||||||||
|
|
||||||||||||||
| getPromptConceptA = () => this.sanitizeConceptForPrompt(this.props.concept) | ||||||||||||||
|
|
||||||||||||||
| getPromptConceptB = () => { | ||||||||||||||
| const baseConcept = this.sanitizeConceptForPrompt(this.props.concept) | ||||||||||||||
| const formValues = this.getValues() | ||||||||||||||
| delete formValues.comment | ||||||||||||||
|
|
||||||||||||||
| return this.sanitizeConceptForPrompt({ | ||||||||||||||
| ...baseConcept, | ||||||||||||||
| ...formValues, | ||||||||||||||
| names: formValues.names || [], | ||||||||||||||
| descriptions: formValues.descriptions || [], | ||||||||||||||
| extras: formValues.extras || {}, | ||||||||||||||
| parent_concept_urls: formValues.parent_concept_urls || [], | ||||||||||||||
| mappings: baseConcept.mappings, | ||||||||||||||
| }) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| generateChangeComment = async () => { | ||||||||||||||
| const { setAlert } = this.context; | ||||||||||||||
| const { t } = this.props | ||||||||||||||
| const aiAssistantURL = this.getAIAssistantURL() | ||||||||||||||
|
|
||||||||||||||
| if (!aiAssistantURL) { | ||||||||||||||
| setAlert({duration: 8000, message: t('concept.ai_assistant_not_configured'), severity: 'error'}) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (!this.hasConceptChanges()) | ||||||||||||||
| return | ||||||||||||||
|
|
||||||||||||||
| this.setState({generatingChangeComment: true}) | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| const response = await APIService.new().request( | ||||||||||||||
| 'POST', | ||||||||||||||
| { | ||||||||||||||
| variables: { | ||||||||||||||
| concept_a: this.getPromptConceptA(), | ||||||||||||||
| concept_b: this.getPromptConceptB(), | ||||||||||||||
| } | ||||||||||||||
| }, | ||||||||||||||
| null, | ||||||||||||||
| { url: `${aiAssistantURL}/prompts/concept-generate-change-comment/$invoke/` } | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| const output = (get(response, 'data.output') || '').trim() | ||||||||||||||
|
|
||||||||||||||
| if (!output) | ||||||||||||||
| throw new Error('No generated comment was returned.') | ||||||||||||||
|
|
||||||||||||||
| this.setFieldValue('comment', output) | ||||||||||||||
| } catch (error) { | ||||||||||||||
| const status = error?.response?.status | ||||||||||||||
| const message = status === 429 ? | ||||||||||||||
| t('concept.try_again_in_a_moment') : | ||||||||||||||
| (error?.response?.data?.detail || error?.response?.data?.error || error?.message || t('common.generic_error')) | ||||||||||||||
|
|
||||||||||||||
| setAlert({duration: 10000, message, severity: 'error'}) | ||||||||||||||
| } finally { | ||||||||||||||
| this.setState({generatingChangeComment: false}) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| getNameStruct = (preferred=false) => { | ||||||||||||||
| const mandatoryFieldStruct = this.getMandatoryFieldStruct() | ||||||||||||||
| const fieldStruct = this.getFieldStruct() | ||||||||||||||
|
|
@@ -191,10 +348,14 @@ class ConceptForm extends FormComponent { | |||||||||||||
| handleSubmit = event => { | ||||||||||||||
| event.preventDefault() | ||||||||||||||
| event.stopPropagation() | ||||||||||||||
| const { edit } = this.props | ||||||||||||||
| const { fields } = this.state | ||||||||||||||
| const isValid = this.setAllFieldsErrors() | ||||||||||||||
| if(isValid) { | ||||||||||||||
| const { setAlert } = this.context; | ||||||||||||||
| const payload = this.getValues() | ||||||||||||||
| if(edit) | ||||||||||||||
| payload.update_comment = fields.comment.value | ||||||||||||||
|
Comment on lines
+357
to
+358
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| let service = APIService.new().overrideURL(this.props.source.url).appendToUrl('concepts/') | ||||||||||||||
| service = this.props.edit ? service.appendToUrl(this.state.fields.id.value + '/').put(payload) : service.post(payload) | ||||||||||||||
| service.then(response => { | ||||||||||||||
|
|
@@ -219,7 +380,15 @@ class ConceptForm extends FormComponent { | |||||||||||||
|
|
||||||||||||||
| render() { | ||||||||||||||
| const { t, edit, repoSummary, repo, concept, onClose } = this.props | ||||||||||||||
| const { conceptClasses, datatypes, locales, nameTypes, descriptionTypes, fields } = this.state | ||||||||||||||
| const { conceptClasses, datatypes, locales, nameTypes, descriptionTypes, fields, generatingChangeComment } = this.state | ||||||||||||||
| const aiAssistantConfigured = Boolean(this.getAIAssistantURL()) | ||||||||||||||
| const canSeeGenerateComment = edit && (isSuperuser() || hasAuthGroup(getCurrentUser(), 'core_user')) | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gates the button to |
||||||||||||||
| const hasConceptChanges = canSeeGenerateComment && this.hasConceptChanges() | ||||||||||||||
| const canGenerateComment = canSeeGenerateComment && aiAssistantConfigured && hasConceptChanges && !generatingChangeComment | ||||||||||||||
| const generateCommentTooltip = !aiAssistantConfigured ? | ||||||||||||||
| t('concept.ai_assistant_not_configured') : | ||||||||||||||
| (!hasConceptChanges ? t('concept.make_change_before_generating') : t('common.generate_with_ai')) | ||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
| <div className='col-xs-12' style={{padding: '8px 16px 12px 16px', height: '100%', overflow: 'auto'}}> | ||||||||||||||
| <div className='col-xs-12 padding-0' style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px'}}> | ||||||||||||||
|
|
@@ -353,6 +522,28 @@ class ConceptForm extends FormComponent { | |||||||||||||
| edit && | ||||||||||||||
| <CardSection title={t('common.update_comment')}> | ||||||||||||||
| <div className='col-xs-12 padding-0' style={{marginTop: '24px'}}> | ||||||||||||||
| { | ||||||||||||||
| canSeeGenerateComment && | ||||||||||||||
| <div style={{display: 'flex', justifyContent: 'flex-end', marginBottom: '8px'}}> | ||||||||||||||
| <Tooltip arrow title={generateCommentTooltip}> | ||||||||||||||
| <span> | ||||||||||||||
| <IconButton | ||||||||||||||
| color='secondary' | ||||||||||||||
| size='small' | ||||||||||||||
| onClick={this.generateChangeComment} | ||||||||||||||
| disabled={!canGenerateComment} | ||||||||||||||
| aria-label={t('concept.generate_comment_aria')} | ||||||||||||||
| > | ||||||||||||||
| { | ||||||||||||||
| generatingChangeComment ? | ||||||||||||||
| <CircularProgress size={18} color='inherit' /> : | ||||||||||||||
| <AutoAwesomeIcon fontSize='small' /> | ||||||||||||||
| } | ||||||||||||||
| </IconButton> | ||||||||||||||
| </span> | ||||||||||||||
| </Tooltip> | ||||||||||||||
| </div> | ||||||||||||||
| } | ||||||||||||||
| <TextField | ||||||||||||||
| id="comment" | ||||||||||||||
| label={t('common.comment')} | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,7 +37,8 @@ | |||||||
| "none": "Ninguno", | ||||||||
| "results": "Resultados", | ||||||||
| "custom": "Personalizado", | ||||||||
| "load_more": "Cargar más" | ||||||||
| "load_more": "Cargar más", | ||||||||
| "generate_with_ai": "Generar con IA" | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| }, | ||||||||
| "errors": { | ||||||||
| "404": "Lo siento, no se pudo encontrar tu página." | ||||||||
|
|
@@ -71,7 +72,11 @@ | |||||||
| "name_and_synonyms": "Nombre y sinónimos", | ||||||||
| "descriptions": "Descripciones", | ||||||||
| "copied_name": "Nombre del concepto y URL copiados al portapapeles", | ||||||||
| "copied_description": "Descripción del concepto y URL copiados al portapapeles" | ||||||||
| "copied_description": "Descripción del concepto y URL copiados al portapapeles", | ||||||||
| "ai_assistant_not_configured": "El asistente de IA no está configurado para este entorno.", | ||||||||
| "try_again_in_a_moment": "Vuelve a intentarlo en un momento.", | ||||||||
| "make_change_before_generating": "Haz un cambio en el concepto antes de generar un comentario.", | ||||||||
| "generate_comment_aria": "Generar comentario con IA" | ||||||||
| }, | ||||||||
| "mapping": { | ||||||||
| "mappings": "Mapeos" | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -94,6 +94,7 @@ | |||||||
| "custom": "自定义", | ||||||||
| "none": "无", | ||||||||
| "load_more": "加载更多", | ||||||||
| "generate_with_ai": "使用 AI 生成", | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here — add the
Suggested change
|
||||||||
| "something_went_wrong": "出了些问题", | ||||||||
| "no_results": "无结果" | ||||||||
| }, | ||||||||
|
|
@@ -171,7 +172,11 @@ | |||||||
| "header": "描述" | ||||||||
| } | ||||||||
| }, | ||||||||
| "edit_concept": "编辑概念" | ||||||||
| "edit_concept": "编辑概念", | ||||||||
| "ai_assistant_not_configured": "此环境尚未配置 AI 助手。", | ||||||||
| "try_again_in_a_moment": "请稍后再试。", | ||||||||
| "make_change_before_generating": "请先更改概念,再生成评论。", | ||||||||
| "generate_comment_aria": "使用 AI 生成评论" | ||||||||
| }, | ||||||||
| "mapping": { | ||||||||
| "mappings": "映射关系", | ||||||||
|
|
||||||||
Uh oh!
There was an error while loading. Please reload this page.