@@ -2,32 +2,155 @@ import { AuthenticationError } from '@app/common/errors/authentication-error'
22import { RunResponse } from '@app/definitions/definition'
33import { OperationAction } from '@app/definitions/opertion-action'
44import { getChatCompletion , getConversationInfo , getUserIntent , UserIntent } from '@chainjet/tools/dist/ai/ai'
5- import { NotFoundException } from '@nestjs/common'
6- import { Menu } from 'apps/api/src/chat/entities/menu'
7- import { MenuService } from 'apps/api/src/chat/services/menu.service'
5+ import { BadRequestException } from '@nestjs/common'
6+ import { AccountCredential } from 'apps/api/src/account-credentials/entities/account-credential'
7+ import { ContactService } from 'apps/api/src/chat/services/contact.service'
8+ import { IntegrationAction } from 'apps/api/src/integration-actions/entities/integration-action'
9+ import { User } from 'apps/api/src/users/entities/user'
10+ import { WorkflowAction } from 'apps/api/src/workflow-actions/entities/workflow-action'
811import { OperationRunOptions } from 'apps/runner/src/services/operation-runner.service'
9- import { JSONSchema7 } from 'json-schema'
12+ import { JSONSchema7 , JSONSchema7Definition } from 'json-schema'
1013import _ from 'lodash'
1114import { XmtpLib } from '../../xmtp/xmtp.lib'
1215
16+ interface Entity {
17+ name : string
18+ type : string
19+ description ?: string
20+ required ?: boolean
21+ store ?: boolean
22+ }
23+
1324export class GetInfoAction extends OperationAction {
1425 key = 'getInfo'
1526 name = 'Get Information'
1627 description = 'Extrac info from the conversation'
1728 version = '1.0.0'
1829
1930 inputs : JSONSchema7 = {
20- required : [ 'conversationId ' ] ,
31+ required : [ 'entities ' ] ,
2132 properties : {
2233 entities : {
34+ title : 'List of Entities' ,
2335 type : 'array' ,
36+ minItems : 1 ,
37+ items : {
38+ type : 'object' ,
39+ required : [ 'name' , 'type' ] ,
40+ properties : {
41+ name : {
42+ type : 'string' ,
43+ title : 'Name' ,
44+ examples : [ 'Job Title' ] ,
45+ 'x-noInterpolation' : true ,
46+ } as JSONSchema7Definition ,
47+ type : {
48+ type : 'string' ,
49+ title : 'Type' ,
50+ default : '' ,
51+ oneOf : [
52+ {
53+ title : 'Text' ,
54+ const : 'text' ,
55+ } ,
56+ {
57+ title : 'Number' ,
58+ const : 'number' ,
59+ } ,
60+ {
61+ title : 'Date' ,
62+ const : 'date' ,
63+ } ,
64+ {
65+ title : 'Time' ,
66+ const : 'time' ,
67+ } ,
68+ {
69+ title : 'Ethereum Address' ,
70+ const : 'address' ,
71+ } ,
72+ {
73+ title : 'Email Address' ,
74+ const : 'email' ,
75+ } ,
76+ ] ,
77+ 'x-noInterpolation' : true ,
78+ } as JSONSchema7Definition ,
79+ description : {
80+ type : 'string' ,
81+ title : 'Description' ,
82+ examples : [ 'The job title of the user' ] ,
83+ description :
84+ 'Description of the information to extract. This helps the AI to understand what to look for. This is needed when the entity name is not intuitive enough.' ,
85+ 'x-noInterpolation' : true ,
86+ } as JSONSchema7Definition ,
87+ required : {
88+ type : 'boolean' ,
89+ title : 'Is required?' ,
90+ description :
91+ "Whether the information is required to continue. If it's required and the user doesn't provide it, the AI will ask for it." ,
92+ } ,
93+ store : {
94+ type : 'boolean' ,
95+ title : 'Store in contact?' ,
96+ description : 'Whether the information should be stored in the contact.' ,
97+ } ,
98+ } ,
99+ } ,
100+ } ,
101+ confirm : {
102+ type : 'boolean' ,
103+ title : 'Ask for confirmation?' ,
104+ description : 'Ask the user to confirm that the information is correct before continuing?' ,
24105 } ,
25106 } ,
26107 }
27108 outputs : JSONSchema7 = {
28109 properties : { } ,
29110 }
30111
112+ async beforeCreate (
113+ workflowAction : Partial < WorkflowAction > ,
114+ integrationAction : IntegrationAction ,
115+ accountCredential : AccountCredential | null ,
116+ ) : Promise < Partial < WorkflowAction > > {
117+ if ( ! Array . isArray ( workflowAction . inputs ?. entities ) || ! workflowAction . inputs ?. entities . length ) {
118+ return workflowAction
119+ }
120+ const entities = workflowAction . inputs . entities as Entity [ ]
121+
122+ // Check for duplicate entity names
123+ const entityNames = entities . map ( ( entity ) => entity . name . toLowerCase ( ) )
124+ const entityNamesSet = new Set ( entityNames )
125+ if ( entityNames . length !== entityNamesSet . size ) {
126+ throw new BadRequestException ( `Duplicate entity names: ${ entityNames . join ( ', ' ) } ` )
127+ }
128+
129+ // Update schema response
130+ workflowAction . schemaResponse = {
131+ type : 'object' ,
132+ properties : entities . reduce ( ( properties : Record < string , any > , entity ) => {
133+ properties [ entity . name ] = {
134+ type : entity . type ,
135+ title : entity . name ,
136+ description : entity . description ,
137+ }
138+ return properties
139+ } , { } ) ,
140+ }
141+
142+ return workflowAction
143+ }
144+
145+ async beforeUpdate (
146+ update : Partial < WorkflowAction > ,
147+ prevWorkflowAction : WorkflowAction ,
148+ integrationAction : IntegrationAction ,
149+ accountCredential : AccountCredential | null ,
150+ ) : Promise < Partial < WorkflowAction > > {
151+ return this . beforeCreate ( update , integrationAction , accountCredential )
152+ }
153+
31154 async run ( { user, inputs, previousOutputs, credentials } : OperationRunOptions ) : Promise < RunResponse > {
32155 if ( ! credentials . keys ) {
33156 throw new AuthenticationError ( `Missing keys for XMTP` )
@@ -44,9 +167,7 @@ export class GetInfoAction extends OperationAction {
44167 content : message . content ,
45168 role : message . from === 'user' ? 'user' : 'assistant' ,
46169 } ) )
47- const entities = inputs . entities
48- const menuIds = entities . filter ( ( entity ) => entity . type === 'order' ) . map ( ( entity ) => entity . menu )
49- const menus = await MenuService . instance . find ( { owner : user , _id : { $in : menuIds } } )
170+ const entities = inputs . entities as Entity [ ]
50171
51172 // If we're confirming the order, check for the message intent
52173 if ( latestOutputs . confirmingOrder ) {
@@ -66,26 +187,17 @@ export class GetInfoAction extends OperationAction {
66187 ]
67188 const intent = await getUserIntent ( confirmIntents , latestMessage ?. content , messages )
68189 if ( intent ?. toLowerCase ( ) === 'confirm' ) {
69- return {
70- outputs : {
71- ... this . getInfoOutputs ( entities , latestOutputs . conversationInfo , menus ) ,
72- } ,
73- learnResponseWorkflow : true ,
74- }
190+ return await this . completeAction (
191+ entities ,
192+ latestOutputs . conversationInfo ,
193+ user as User ,
194+ previousOutputs ?. contact ?. address ,
195+ )
75196 }
76197 }
77198
78199 const originalInputs = _ . cloneDeep ( inputs )
79200
80- for ( const entity of entities ) {
81- if ( entity . type === 'order' ) {
82- entity . menu = menus . find ( ( menu ) => menu . _id . toString ( ) === entity . menu . toString ( ) )
83- if ( ! entity . menu ) {
84- throw new NotFoundException ( `Menu ${ entity . menu } not found` )
85- }
86- }
87- }
88-
89201 const data = await getConversationInfo ( entities , messages )
90202 if ( data . FollowUp ) {
91203 const client = await XmtpLib . getClient ( credentials . keys , credentials . env ?? 'production' )
@@ -101,11 +213,14 @@ export class GetInfoAction extends OperationAction {
101213 }
102214
103215 if ( inputs . confirm ) {
104- const orderList = this . getOrderList ( entities , data ) . trim ( )
105- const prompt = `Your task is to confirm with the user if the following order is correct.\n${ orderList } `
216+ const confirmList = entities
217+ . filter ( ( entity ) => data [ entity . name ] )
218+ . map ( ( entity ) => `${ entity . name } : ${ data [ entity . name ] } ` )
219+ . join ( '\n' )
220+ const prompt = `Your task is to confirm with the user if the following information is correct.\n${ confirmList } `
106221 let content = await getChatCompletion ( [ { role : 'system' , content : prompt } , ...messages ] )
107222 if ( ! content ) {
108- content = `Please confirm if the request is correct:\n${ orderList } `
223+ content = `Please confirm if this information is correct:\n${ confirmList } `
109224 }
110225 const client = await XmtpLib . getClient ( credentials . keys , credentials . env ?? 'production' )
111226 const conversationId = previousOutputs ?. trigger . conversation . id
@@ -122,51 +237,33 @@ export class GetInfoAction extends OperationAction {
122237 }
123238 }
124239
125- return {
126- outputs : {
127- ...this . getInfoOutputs ( entities , data , menus ) ,
128- } ,
129- learnResponseWorkflow : true ,
130- }
240+ return await this . completeAction ( entities , data , user as User , previousOutputs ?. contact ?. address )
131241 }
132242
133- getOrderList ( entities : any [ ] , data : any ) {
134- let orderList = ''
135- for ( const entity of entities ) {
136- if ( ! data [ entity . name ] ) {
137- continue
138- }
139- if ( entity . type === 'order' ) {
140- orderList += data [ entity . name ] . map ( ( item ) => `${ item . quantity ?? 1 } ${ item . item } ` ) . join ( '\n' ) + '\n'
141- } else {
142- orderList += `${ entity . name } : ${ data [ entity . name ] } \n`
243+ async completeAction ( entities : Entity [ ] , data : any , user : User , contactAddress ?: string ) {
244+ const storeEntities = entities . filter ( ( entity ) => entity . store )
245+ if ( storeEntities . length && contactAddress ) {
246+ const entitiesData = storeEntities . reduce ( ( fields : Record < string , any > , entity ) => {
247+ fields [ entity . name ] = data [ entity . name ]
248+ return fields
249+ } , { } )
250+ const contact = await ContactService . instance . findOne ( { address : contactAddress , owner : user } )
251+ if ( contact ) {
252+ await ContactService . instance . updateOne ( contact . id , {
253+ fields : {
254+ ...( contact . fields ?? { } ) ,
255+ ...entitiesData ,
256+ } ,
257+ } )
143258 }
144259 }
145- return orderList
146- }
147-
148- getInfoOutputs ( entities : any [ ] , data : any , menus : Menu [ ] ) {
149- const outputs : Record < string , any > = { }
150- for ( const entity of entities ) {
151- if ( data [ entity . name ] ) {
152- if ( entity . type === 'order' ) {
153- const menu = menus . find ( ( menu ) => menu . _id . toString ( ) === entity . menu . toString ( ) )
154- const total = data [ entity . name ] . reduce ( ( acc , item ) => {
155- const menuItem = menu ! . items . find ( ( menuItem ) => menuItem . name === item . item )
156- return acc + ( menuItem ?. price ?? 0 ) * ( item . quantity ?? 1 )
157- } , 0 )
158- outputs [ entity . name ] = {
159- ...( total && { total, currency : menu ?. currency } ) ,
160- items : data [ entity . name ] . map ( ( item ) => ( {
161- name : item . item ,
162- quantity : item . quantity ,
163- } ) ) ,
164- }
165- } else {
260+ return {
261+ outputs : entities . reduce ( ( outputs : Record < string , any > , entity ) => {
262+ if ( data [ entity . name ] ) {
166263 outputs [ entity . name ] = data [ entity . name ]
167264 }
168- }
265+ return outputs
266+ } , { } ) ,
169267 }
170- return outputs
171268 }
172269}
0 commit comments