Skip to content

Commit 265a25a

Browse files
committed
feat: update get conversation info action
1 parent ff60f3b commit 265a25a

File tree

6 files changed

+196
-99
lines changed

6 files changed

+196
-99
lines changed

apps/runner/src/utils/input.utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function parseInput(input: unknown, outputs: Record<string, Record<string
6060
*/
6161
export function calculateExpression(input: string, references: Record<string, Record<string, unknown>>): unknown {
6262
// each "[\w[\]]" group matches the allowed characters for variables, including array access (e.g. a.b[0].c)
63-
const operatorsRegex = /[\w[\]]+\.[\w[\]]+(\.[\w[\]]+)*/g
63+
const operatorsRegex = /[\w\s[\]]+(\.[\w\s[\]]+|\[\d+\])*/g
6464

6565
const operators = [...input.matchAll(operatorsRegex)]
6666

generated/graphql.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,20 +1055,6 @@ export interface IntegrationTriggersConnection {
10551055
edges: IntegrationTriggerEdge[];
10561056
}
10571057

1058-
export interface MenuItem {
1059-
name: string;
1060-
price: number;
1061-
}
1062-
1063-
export interface Menu {
1064-
id: string;
1065-
createdAt: DateTime;
1066-
name: string;
1067-
currency?: Nullable<string>;
1068-
items: MenuItem[];
1069-
order: Menu;
1070-
}
1071-
10721058
export interface Contact {
10731059
id: string;
10741060
createdAt: DateTime;
@@ -1086,6 +1072,20 @@ export interface AccountCredential {
10861072
authExpired: boolean;
10871073
}
10881074

1075+
export interface MenuItem {
1076+
name: string;
1077+
price: number;
1078+
}
1079+
1080+
export interface Menu {
1081+
id: string;
1082+
createdAt: DateTime;
1083+
name: string;
1084+
currency?: Nullable<string>;
1085+
items: MenuItem[];
1086+
order: Menu;
1087+
}
1088+
10891089
export interface OrderItem {
10901090
name: string;
10911091
quantity: number;

generated/schema.graphql

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -408,20 +408,6 @@ type IntegrationTriggersConnection {
408408
edges: [IntegrationTriggerEdge!]!
409409
}
410410

411-
type MenuItem {
412-
name: String!
413-
price: Float!
414-
}
415-
416-
type Menu {
417-
id: ID!
418-
createdAt: DateTime!
419-
name: String!
420-
currency: String
421-
items: [MenuItem!]!
422-
order: Menu!
423-
}
424-
425411
type Contact {
426412
id: ID!
427413
createdAt: DateTime!
@@ -439,6 +425,20 @@ type AccountCredential {
439425
authExpired: Boolean!
440426
}
441427

428+
type MenuItem {
429+
name: String!
430+
price: Float!
431+
}
432+
433+
type Menu {
434+
id: ID!
435+
createdAt: DateTime!
436+
name: String!
437+
currency: String
438+
items: [MenuItem!]!
439+
order: Menu!
440+
}
441+
442442
type OrderItem {
443443
name: String!
444444
quantity: Float!

libs/definitions/src/integration-definitions/chatbot/actions/get-info.action.ts

Lines changed: 162 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,155 @@ import { AuthenticationError } from '@app/common/errors/authentication-error'
22
import { RunResponse } from '@app/definitions/definition'
33
import { OperationAction } from '@app/definitions/opertion-action'
44
import { 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'
811
import { OperationRunOptions } from 'apps/runner/src/services/operation-runner.service'
9-
import { JSONSchema7 } from 'json-schema'
12+
import { JSONSchema7, JSONSchema7Definition } from 'json-schema'
1013
import _ from 'lodash'
1114
import { 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+
1324
export 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
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@aws-sdk/client-cloudwatch": "^3.146.0",
3333
"@aws-sdk/client-ses": "^3.145.0",
3434
"@celo-tools/celo-ethers-wrapper": "0.3.0",
35-
"@chainjet/tools": "0.0.7",
35+
"@chainjet/tools": "0.0.8",
3636
"@googleapis/drive": "^3.0.1",
3737
"@googleapis/sheets": "^3.0.2",
3838
"@mailchain/sdk": "0.18.6",

0 commit comments

Comments
 (0)