From badbb37469efbfa1e37c6b7720256d9022758b6c Mon Sep 17 00:00:00 2001 From: jiyoung Date: Tue, 15 Oct 2024 11:16:19 +0900 Subject: [PATCH 01/15] feat: improve assistant metadata and metrics. --- src/ai/assistant.ts | 345 ++++++++++++++++++++++---------------- src/constants.ts | 5 + src/types.ts | 103 ++++++++---- src/utils/util.ts | 54 ++++-- test/ai/assistant.test.ts | 78 +++++---- 5 files changed, 348 insertions(+), 237 deletions(-) diff --git a/src/ai/assistant.ts b/src/ai/assistant.ts index c0b7df33..4b772e0a 100644 --- a/src/ai/assistant.ts +++ b/src/ai/assistant.ts @@ -1,11 +1,10 @@ -import _ from 'lodash'; +import _ from "lodash"; -import FactoryBase from '../factoryBase'; -import AinftObject from '../ainft721Object'; -import { OperationType, getServiceName, request } from '../utils/ainize'; +import FactoryBase from "../factoryBase"; +import AinftObject from "../ainft721Object"; +import { OperationType, getServiceName, request } from "../utils/ainize"; import { Assistant, - AssistantCreateOptions, AssistantCreateParams, AssistantDeleteTransactionResult, AssistantDeleted, @@ -14,16 +13,16 @@ import { NftToken, NftTokens, QueryParamsWithoutSort, -} from '../types'; +} from "../types"; import { MESSAGE_GC_MAX_SIBLINGS, MESSAGE_GC_NUM_SIBLINGS_DELETED, THREAD_GC_MAX_SIBLINGS, THREAD_GC_NUM_SIBLINGS_DELETED, WHITELISTED_OBJECT_IDS, -} from '../constants'; -import { getEnv } from '../utils/env'; -import { Path } from '../utils/path'; +} from "../constants"; +import { getEnv } from "../utils/env"; +import { Path } from "../utils/path"; import { buildSetValueOp, buildSetWriteRuleOp, @@ -31,8 +30,8 @@ import { buildSetOp, buildSetTxBody, sendTx, -} from '../utils/transaction'; -import { getChecksumAddress, getAssistant, getToken } from '../utils/util'; +} from "../utils/transaction"; +import { getChecksumAddress, getAssistant, getToken, arrayToObject } from "../utils/util"; import { isObjectOwner, validateAssistant, @@ -40,13 +39,13 @@ import { validateObject, validateServerConfigurationForObject, validateToken, -} from '../utils/validator'; -import { authenticated } from '../utils/decorator'; -import { AinftError } from '../error'; +} from "../utils/validator"; +import { authenticated } from "../utils/decorator"; +import { AinftError } from "../error"; enum Role { - OWNER = 'owner', - USER = 'user', + OWNER = "owner", + USER = "user", } /** @@ -56,75 +55,68 @@ enum Role { export class Assistants extends FactoryBase { /** * Create an assistant with a model and instructions. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. + * @param {string} objectId - The ID of the AINFT object. + * @param {string} tokenId - The ID of the AINFT token. * @param {AssistantCreateParams} AssistantCreateParams - The parameters to create assistant. - * @param {AssistantCreateOptions} AssistantCreateOptions - The creation options. * @returns A promise that resolves with both the transaction result and the created assistant. */ @authenticated async create( objectId: string, tokenId: string, - { model, name, instructions, description, metadata }: AssistantCreateParams, - options: AssistantCreateOptions = {} + params: AssistantCreateParams ): Promise { const address = await this.ain.signer.getAddress(); - const appId = AinftObject.getAppId(objectId); - const token = await getToken(this.ain, appId, tokenId); - // TODO(jiyoung): limit character count for 'instruction' and 'description'. + // TODO(jiyoung): check character count for input params. await validateObject(this.ain, objectId); await validateToken(this.ain, objectId, tokenId); await validateDuplicateAssistant(this.ain, objectId, tokenId); + // TODO(jiyoung): fix it. // NOTE(jiyoung): creation is limited to owners, except for whitelisted objects. const role = (await isObjectOwner(this.ain, objectId, address)) ? Role.OWNER : Role.USER; - const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); - if (!whitelisted && role !== Role.OWNER) { - throw new AinftError('permission-denied', `cannot create assistant for ${objectId}`); + const isWhitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); + if (!isWhitelisted && role !== Role.OWNER) { + throw new AinftError("permission-denied", `cannot create assistant for ${objectId}`); } + const token = await getToken(this.ain, objectId, tokenId); const serviceName = getServiceName(); await validateServerConfigurationForObject(this.ain, objectId, serviceName); const opType = OperationType.CREATE_ASSISTANT; - const body = { - role, - objectId, - tokenId, - model, - name, - instructions, - ...(description && { description }), - ...(metadata && !_.isEmpty(metadata) && { metadata }), - ...(options && !_.isEmpty(options) && { options }), - }; + const body = this.buildReqBodyForCreateAssistant(objectId, tokenId, role, params); - const { data } = await request(this.ainize!, { + const { data } = await request(this.ainize!, { serviceName, opType, data: body, }); - const assistant = { + const assistant: Assistant = { id: data.id, - objectId: objectId, - tokenId: tokenId, - owner: token.owner, + createdAt: data.createdAt, + objectId: data.objectId, + tokenId: data.tokenId, + tokenOwner: token.owner, model: data.model, name: data.name, - instructions: data.instructions, description: data.description, - metadata: data.metadata, - created_at: data.created_at, - metric: { - numThreads: 0, + instructions: data.instructions, + metadata: { + author: data.metadata?.author || null, + bio: data.metadata?.bio || null, + chatStarter: data.metadata?.chatStarter ? Object.values(data.metadata?.chatStarter) : null, + greetingMessage: data.metadata?.greetingMessage || null, + image: data.metadata?.image || null, + tags: data.metadata?.tags ? Object.values(data.metadata?.tags) : null, }, + metrics: data.metrics || {}, }; if (role === Role.OWNER) { - const txBody = this.buildTxBodyForCreateAssistant(address, objectId, tokenId, data); + const txBody = this.buildTxBodyForCreateAssistant(data, address); const result = await sendTx(txBody, this.ain); return { ...result, assistant }; } else { @@ -134,9 +126,9 @@ export class Assistants extends FactoryBase { /** * Updates an assistant. - * @param {string} objectId - The ID of AINFT object. - * @param {string} tokenId - The ID of AINFT token. - * @param {string} assistantId - The ID of assistant. + * @param {string} objectId - The ID of the AINFT object. + * @param {string} tokenId - The ID of the AINFT token. + * @param {string} assistantId - The ID of the assistant. * @param {AssistantUpdateParams} AssistantUpdateParams - The parameters to update assistant. * @returns A promise that resolves with both the transaction result and the updated assistant. */ @@ -145,7 +137,7 @@ export class Assistants extends FactoryBase { objectId: string, tokenId: string, assistantId: string, - { model, name, instructions, description, metadata }: AssistantUpdateParams + params: AssistantUpdateParams ): Promise { const address = await this.ain.signer.getAddress(); @@ -158,37 +150,48 @@ export class Assistants extends FactoryBase { const role = (await isObjectOwner(this.ain, objectId, address)) ? Role.OWNER : Role.USER; const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); if (!whitelisted && role !== Role.OWNER) { - throw new AinftError('permission-denied', `cannot update assistant for ${objectId}`); + throw new AinftError("permission-denied", `cannot update assistant for ${objectId}`); } + const token = await getToken(this.ain, objectId, tokenId); const serviceName = getServiceName(); await validateServerConfigurationForObject(this.ain, objectId, serviceName); const opType = OperationType.MODIFY_ASSISTANT; - const body = { - role, - objectId, - tokenId, - assistantId, - ...(model && { model }), - ...(name && { name }), - ...(instructions && { instructions }), - ...(description && { description }), - ...(metadata && !_.isEmpty(metadata) && { metadata }), - }; - - const { data } = await request(this.ainize!, { + const body = this.buildReqBodyForUpdateAssistant(objectId, tokenId, assistantId, role, params); + const { data } = await request(this.ainize!, { serviceName, opType, data: body, }); + const assistant: Assistant = { + id: data.id, + createdAt: data.createdAt, + objectId: data.objectId, + tokenId: data.tokenId, + tokenOwner: token.owner, + model: data.model, + name: data.name, + description: data.description, + instructions: data.instructions, + metadata: { + author: data.metadata?.author || null, + bio: data.metadata?.bio || null, + chatStarter: data.metadata?.chatStarter ? Object.values(data.metadata?.chatStarter) : null, + greetingMessage: data.metadata?.greetingMessage || null, + image: data.metadata?.image || null, + tags: data.metadata?.tags ? Object.values(data.metadata?.tags) : null, + }, + metrics: data.metrics || {}, + }; + if (role === Role.OWNER) { const txBody = this.buildTxBodyForUpdateAssistant(address, objectId, tokenId, data); const result = await sendTx(txBody, this.ain); - return { ...result, assistant: data }; + return { ...result, assistant }; } else { - return { assistant: data }; + return { assistant }; } } @@ -215,7 +218,7 @@ export class Assistants extends FactoryBase { const role = (await isObjectOwner(this.ain, objectId, address)) ? Role.OWNER : Role.USER; const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); if (!whitelisted && role !== Role.OWNER) { - throw new AinftError('permission-denied', `cannot delete assistant for ${objectId}`); + throw new AinftError("permission-denied", `cannot delete assistant for ${objectId}`); } const serviceName = getServiceName(); @@ -251,23 +254,35 @@ export class Assistants extends FactoryBase { await validateObject(this.ain, objectId); await validateToken(this.ain, objectId, tokenId); - const assistant = await getAssistant(this.ain, appId, tokenId); - const token = await getToken(this.ain, appId, tokenId); + const _assistant = await getAssistant(this.ain, appId, tokenId); + const _token = await getToken(this.ain, objectId, tokenId); - const data = { - id: assistant.id, + const assistant: Assistant = { + id: _assistant.id, + createdAt: _assistant.createdAt, objectId, tokenId, - owner: token.owner, - model: assistant.config.model, - name: assistant.config.name, - instructions: assistant.config.instructions, - description: assistant.config.description || null, - metadata: assistant.config.metadata || {}, - created_at: assistant.createdAt, + tokenOwner: _token.owner, + model: _assistant.config.model, + name: _assistant.config.name, + description: _assistant.config.description || null, + instructions: _assistant.config.instructions || null, + metadata: { + author: _assistant.config.metadata?.author || null, + bio: _assistant.config.metadata?.bio || null, + chatStarter: _assistant.config.metadata?.chatStarter + ? Object.values(_assistant.config.metadata?.chatStarter) + : null, + greetingMessage: _assistant.config.metadata?.greetingMessage || null, + image: _assistant.config.metadata?.image || null, + tags: _assistant.config.metadata?.tags + ? Object.values(_assistant.config.metadata?.tags) + : null, + }, + metrics: _assistant.metrics || {}, }; - return data; + return assistant; } /** @@ -280,14 +295,13 @@ export class Assistants extends FactoryBase { async list( objectIds: string[], address?: string | null, - { limit = 20, offset = 0, order = 'desc' }: QueryParamsWithoutSort = {} + { limit = 20, offset = 0, order = "desc" }: QueryParamsWithoutSort = {} ) { await Promise.all(objectIds.map((objectId) => validateObject(this.ain, objectId))); const allAssistants = await Promise.all( objectIds.map(async (objectId) => { - const tokens = await this.getTokens(objectId, address); - return this.getAssistantsFromTokens(tokens); + return this.getAssistants(objectId, address); }) ); const assistants = allAssistants.flat(); @@ -306,7 +320,7 @@ export class Assistants extends FactoryBase { const whitelisted = WHITELISTED_OBJECT_IDS[getEnv()].includes(objectId); if (!whitelisted) { throw new AinftError( - 'forbidden', + "forbidden", `cannot mint token for ${objectId}. please use the Ainft721Object.mint() function instead.` ); } @@ -324,30 +338,74 @@ export class Assistants extends FactoryBase { return data; } - private buildTxBodyForCreateAssistant( - address: string, + // NOTE(jiyoung): transform metadata by recursively converting arrays to objects + // and replacing empty arrays or objects with null. + private transformMetadata(metadata: any) { + if (Array.isArray(metadata)) { + // 1-1. empty array + if (metadata.length === 0) { + return null; + } + // 1-2. array to object + const result: { [key: string]: any } = {}; + metadata.forEach((v, i) => { + result[`${i}`] = this.transformMetadata(v); + }); + return result; + } else if (typeof metadata === "object" && metadata !== null) { + // 2-1. empty object + if (Object.keys(metadata).length === 0) { + return null; + } + // 2-2. nested object + const result: { [key: string]: any } = {}; + for (const key in metadata) { + result[key] = this.transformMetadata(metadata[key]); + } + return result; + } + return metadata; + } + + private buildReqBodyForCreateAssistant( objectId: string, tokenId: string, - { id, model, name, instructions, description, metadata, created_at }: Assistant + role: Role, + params: AssistantCreateParams ) { - const appId = AinftObject.getAppId(objectId); - const assistantPath = Path.app(appId).token(tokenId).ai().value(); - const historyPath = `/apps/${appId}/tokens/${tokenId}/ai/history/$user_addr`; + return { + role, + objectId, + tokenId, + model: params.model, + name: params.name, + description: params.description || null, + instructions: params.instructions || null, + metadata: this.transformMetadata(params.metadata), + options: { + autoImage: params.autoImage || false, + }, + }; + } + + private buildTxBodyForCreateAssistant(assistant: Assistant, address: string) { + const appId = AinftObject.getAppId(assistant.objectId); + const assistantPath = Path.app(appId).token(assistant.tokenId).ai().value(); + const historyPath = `/apps/${appId}/tokens/${assistant.tokenId}/ai/history/$user_addr`; const threadPath = `${historyPath}/threads/$thread_id`; const messagePath = `${threadPath}/messages/$message_id`; - const config = { - model, - name, - instructions, - ...(description && { description }), - ...(metadata && !_.isEmpty(metadata) && { metadata }), - }; - const info = { - id, - type: 'chat', - config, - createdAt: created_at, + const value = { + id: assistant.id, + createdAt: assistant.createdAt, + config: { + model: assistant.model, + name: assistant.name, + ...(assistant.instructions && { instructions: assistant.instructions }), + ...(assistant.description && { description: assistant.description }), + ...(assistant.metadata && + !_.isEmpty(assistant.metadata) && { metadata: assistant.metadata }), + }, history: true, }; @@ -365,7 +423,7 @@ export class Assistants extends FactoryBase { }, }; - const setAssistantInfoOp = buildSetValueOp(assistantPath, info); + const setAssistantInfoOp = buildSetValueOp(assistantPath, value); const setHistoryWriteRuleOp = buildSetWriteRuleOp(historyPath, rules.write); const setThreadGCRuleOp = buildSetStateRuleOp(threadPath, rules.state.thread); const setMessageGCRuleOp = buildSetStateRuleOp(messagePath, rules.state.message); @@ -381,6 +439,26 @@ export class Assistants extends FactoryBase { ); } + private buildReqBodyForUpdateAssistant( + objectId: string, + tokenId: string, + assistantId: string, + role: Role, + params: AssistantUpdateParams + ) { + return { + role, + objectId, + tokenId, + assistantId, + ...(params.model && { model: params.model }), + ...(params.name && { name: params.name }), + ...(params.description && { description: params.description }), + ...(params.instructions && { instructions: params.instructions }), + metadata: this.transformMetadata(params.metadata), + }; + } + private buildTxBodyForUpdateAssistant( address: string, objectId: string, @@ -424,59 +502,28 @@ export class Assistants extends FactoryBase { ); } - private async getTokens(objectId: string, address?: string | null) { + private async getAssistants(objectId: string, address?: string | null) { const appId = AinftObject.getAppId(objectId); const tokensPath = Path.app(appId).tokens().value(); const tokens: NftTokens = (await this.ain.db.ref(tokensPath).getValue()) || {}; - return Object.entries(tokens).reduce((acc, [id, token]) => { - if (!address || token.owner === address) { - acc.push({ objectId, tokenId: id, ...token }); - } - return acc; - }, []); - } - private getAssistantsFromTokens(tokens: NftToken[]) { - return tokens.reduce((acc, token) => { - if (token.ai) { - acc.push(this.toAssistant(token, this.countThreads(token.ai.history))); - } - return acc; - }, []); - } + const assistants = await Promise.all( + Object.entries(tokens).map(async ([id, token]) => { + if (!address || token.owner === address) { + return await getAssistant(this.ain, appId, id); + } + return null; + }) + ); - private toAssistant(data: any, numThreads: number): Assistant { - return { - id: data.ai.id, - objectId: data.objectId, - tokenId: data.tokenId, - owner: data.owner, - model: data.ai.config.model, - name: data.ai.config.name, - instructions: data.ai.config.instructions, - description: data.ai.config.description || null, - metadata: data.ai.config.metadata || {}, - created_at: data.ai.createdAt, - metric: { numThreads }, - }; + return assistants.filter((assistant) => assistant !== null); } - private sortAssistants(assistants: Assistant[], order: 'asc' | 'desc') { + private sortAssistants(assistants: Assistant[], order: "asc" | "desc") { return assistants.sort((a, b) => { - if (a.created_at < b.created_at) return order === 'asc' ? -1 : 1; - if (a.created_at > b.created_at) return order === 'asc' ? 1 : -1; + if (a.createdAt < b.createdAt) return order === "asc" ? -1 : 1; + if (a.createdAt > b.createdAt) return order === "asc" ? 1 : -1; return 0; }); } - - private countThreads(items: any) { - if (typeof items !== 'object' || !items) { - return 0; - } - return Object.values(items).reduce((sum: number, item: any) => { - const count = - item.threads && typeof item.threads === 'object' ? Object.keys(item.threads).length : 0; - return sum + count; - }, 0); - } } diff --git a/src/constants.ts b/src/constants.ts index d5c736ad..d99d118e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,11 @@ export const AIN_BLOCKCHAIN_ENDPOINT = { prod: 'https://mainnet-api.ainetwork.ai', }; +export const AGENT_API_ENDPOINT = { + dev: 'https://aina-backend-dev.ainetwork.xyz', + prod: 'https://aina-backend.ainetwork.xyz', +}; + export const AIN_BLOCKCHAIN_CHAIN_ID = { dev: 0, prod: 1 } as const; export const MIN_GAS_PRICE = 500; diff --git a/src/types.ts b/src/types.ts index 07a12f36..78a4277d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1253,29 +1253,67 @@ export interface AiConfiguration { export interface Assistant { /** The identifier. */ id: string; - /** The ID of AINFT object. */ - objectId: string | null; - /** The ID of AINFT token. */ - tokenId: string | null; - /** The owner address of AINFT token. */ - owner: string | null; + /** The Unix timestamp in seconds for when the assistant was created. */ + createdAt: number; + /** The ID of the AINFT object. */ + objectId: string; + /** The ID of the AINFT token. */ + tokenId: string; + /** The owner address of the AINFT token. */ + tokenOwner: string; /** The name of the model to use. */ model: string; /** The name of the assistant. */ name: string; - /** The system instructions that the assistant uses. The maximum length is 32768 characters. */ - instructions: string; /** The description of the assistant. The maximum length is 512 characters. */ description: string | null; - /** - * The metadata can contain up to 16 pairs, - * with keys limited to 64 characters and values to 512 characters. - */ - metadata: object | null; - /** The metric of the assistant. */ - metric?: { [key: string]: number } | null; - /** The UNIX timestamp in seconds. */ - created_at: number; + /** The system instructions that the assistant uses. The maximum length is 10000 characters. */ + instructions: string | null; + /** Any metadata related to the assistant. */ + metadata: AssistantMetadata | null; + /** The metrics of the assistant. */ + metrics: AssistantMetrics | null; +} + +export interface AssistantAuthor { + /** The address of the author. */ + address?: string; + /** The username of the author. */ + username?: string; + /** The image of the author. */ + picture?: string; +} + +export interface AssistantMetadata extends Metadata { + /** The author of the assistant. */ + author?: AssistantAuthor | null; + /** The bio of the assistant. */ + bio?: string | null; + /** The chat starter of the assistant. */ + chatStarter?: string[] | null; + /** The greeting message of the assistant. */ + greetingMessage?: string | null; + /** The image of the assistant. */ + image?: string | null; + /** The tags of the assistant. */ + tags?: string[] | null; +} + +export interface AssistantMetrics { + /** The number of API calls. */ + numCalls?: number; + /** The number of conversations. */ + numThreads?: number; + /** The number of users. */ + numUsers?: number; + /** The total credits used by users. */ + totalUsedCredits?: number; + /** The total revenue of the assistant. */ + totalRevenue?: number; + /** The ad revenue of the assistant. */ + adRevenue?: number; + /** The platform revenue of the assistant. */ + platformRevenue?: number; } export interface AssistantDeleted { @@ -1288,38 +1326,29 @@ export interface AssistantDeleted { export interface AssistantCreateParams { /** The name of the model to use. */ model: Model; - /** The name of the assistant. The maximum length is 256 characters. */ + /** The name of the assistant. */ name: string; - /** The system instructions that the assistant uses. The maximum length is 32768 characters. */ - instructions: string; /** The description of the assistant. The maximum length is 512 characters. */ description?: string | null; - /** - * The metadata can contain up to 16 pairs, - * with keys limited to 64 characters and values to 512 characters. - */ - metadata?: object | null; -} - -export interface AssistantCreateOptions { + /** The system instructions that the assistant uses. The maximum length is 10000 characters. */ + instructions?: string | null; + /** Any metadata related to the assistant. */ + metadata?: AssistantMetadata | null; /** If true, automatically set the profile image for the assistant. */ - image?: boolean; + autoImage?: boolean; } export interface AssistantUpdateParams { /** The name of the model to use. */ model?: Model; - /** The name of the assistant. The maximum length is 256 characters. */ + /** The name of the assistant. */ name?: string | null; - /** The system instructions that the assistant uses. The maximum length is 32768 characters. */ - instructions?: string | null; /** The description of the assistant. The maximum length is 512 characters. */ description?: string | null; - /** - * The metadata can contain up to 16 pairs, - * with keys limited to 64 characters and values to 512 characters. - */ - metadata?: object | null; + /** The system instructions that the assistant uses. The maximum length is 10000 characters. */ + instructions?: string | null; + /** Any metadata related to the assistant. */ + metadata?: AssistantMetadata | null; } export interface Thread { diff --git a/src/utils/util.ts b/src/utils/util.ts index 15055e8a..7d710d88 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,17 +1,19 @@ -import stringify = require('fast-json-stable-stringify'); -import Ain from '@ainblockchain/ain-js'; +import stringify = require("fast-json-stable-stringify"); +import Ain from "@ainblockchain/ain-js"; import { SetOperation, SetMultiOperation, TransactionInput, GetOptions, -} from '@ainblockchain/ain-js/lib/types'; -import * as ainUtil from '@ainblockchain/ain-util'; +} from "@ainblockchain/ain-js/lib/types"; +import * as ainUtil from "@ainblockchain/ain-util"; -import { MIN_GAS_PRICE } from '../constants'; -import { HttpMethod } from '../types'; -import { Path } from './path'; -import { AinftError } from '../error'; +import { AGENT_API_ENDPOINT, MIN_GAS_PRICE } from "../constants"; +import { HttpMethod } from "../types"; +import { Path } from "./path"; +import { AinftError } from "../error"; +import axios from "axios"; +import { getEnv } from "./env"; export const buildData = ( method: HttpMethod, @@ -30,9 +32,9 @@ export const buildData = ( } if (method === HttpMethod.POST || method === HttpMethod.PUT) { - _data['body'] = stringify(data); + _data["body"] = stringify(data); } else { - _data['querystring'] = stringify(data); + _data["querystring"] = stringify(data); } return _data; @@ -45,7 +47,7 @@ export function sleep(ms: number) { } export function serializeEndpoint(endpoint: string) { - return endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint; + return endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint; } export const valueExists = async (ain: Ain, path: string): Promise => { @@ -57,17 +59,29 @@ export const getAssistant = async (ain: Ain, appId: string, tokenId: string) => // const appId = AinftObject.getAppId(objectId); const assistantPath = Path.app(appId).token(tokenId).ai().value(); const assistant = await getValue(ain, assistantPath); - if (!assistant) { - throw new AinftError('not-found', `assistant not found: ${appId}(${tokenId})`); - } + if (!assistant) { return null } + // TODO(jiyoung): hide api endpoint. + const response = await axios.get(`${AGENT_API_ENDPOINT[getEnv()]}/agents/${assistant.id}`); + const data = response.data; + assistant.metrics = { + numCalls: data?.metrics?.num_calls || null, + numThreads: data?.metrics?.num_threads || null, + numUsers: data?.metrics?.num_users || null, + totalUsedCredits: data?.metrics?.total_used_credits || null, + totalRevenue: data?.metrics?.total_revenue || null, + adRevenue: data?.metrics?.ad_revenue || null, + platformRevenue: data?.metrics?.platform_revenue || null, + }; + return assistant; }; -export const getToken = async (ain: Ain, appId: string, tokenId: string) => { +export const getToken = async (ain: Ain, objectId: string, tokenId: string) => { + const appId = `ainft721_${objectId.toLowerCase()}`; const tokenPath = Path.app(appId).token(tokenId).value(); const token = await getValue(ain, tokenPath); if (!token) { - throw new AinftError('not-found', `token not found: ${appId}(${tokenId})`); + throw new AinftError("not-found", `token ${tokenId} not found for ${objectId}`); } return token; }; @@ -83,3 +97,11 @@ export const getChecksumAddress = (address: string): string => { export const isJoiError = (error: any) => { return error.response?.data?.isJoiError === true; }; + +export const arrayToObject = (array: T[]): { [key: string]: T } => { + const result: { [key: string]: T } = {}; + array.forEach((v, i) => { + result[i.toString()] = v; + }); + return result; +}; diff --git a/test/ai/assistant.test.ts b/test/ai/assistant.test.ts index 88c5806f..d6528401 100644 --- a/test/ai/assistant.test.ts +++ b/test/ai/assistant.test.ts @@ -1,15 +1,15 @@ -import AinftJs from '../../src/ainft'; -import { address, assistantId, objectId, privateKey, tokenId } from '../test_data'; -import { ASSISTANT_REGEX, TX_HASH_REGEX } from '../constants'; +import AinftJs from "../../src/ainft"; +import { address, assistantId, objectId, privateKey, tokenId } from "../test_data"; +import { ASSISTANT_REGEX, TX_HASH_REGEX } from "../constants"; -describe.skip('assistant', () => { +describe("assistant", () => { let ainft: AinftJs; beforeAll(async () => { ainft = new AinftJs({ privateKey, - baseUrl: 'https://ainft-api-dev.ainetwork.ai', - blockchainUrl: 'https://testnet-api.ainetwork.ai', + baseUrl: "https://ainft-api-dev.ainetwork.ai", + blockchainUrl: "https://testnet-api.ainetwork.ai", chainId: 0, }); await ainft.connect(); @@ -19,34 +19,50 @@ describe.skip('assistant', () => { await ainft.disconnect(); }); - it('should create assistant', async () => { + it("should create assistant", async () => { const result = await ainft.assistant.create(objectId, tokenId, { - model: 'gpt-4o-mini', - name: 'name', - instructions: 'instructions', - description: 'description', - metadata: { key1: 'value1' }, + model: "gpt-4o", + name: "name", + description: "description", + instructions: null, + metadata: { + author: { + address: "0xabc123", + username: "username", + picture: "https://example.com/image.png", + }, + bio: "bio", + chatStarter: ["chat_starter_1", "chat_starter_2"], + greetingMessage: "hello", + image: "https://example.com/image.png", + tags: ["tag1", "tag2"], + }, + autoImage: false, }); expect(result.tx_hash).toMatch(TX_HASH_REGEX); expect(result.result).toBeDefined(); expect(result.assistant.id).toMatch(ASSISTANT_REGEX); - expect(result.assistant.model).toBe('gpt-4o-mini'); - expect(result.assistant.name).toBe('name'); - expect(result.assistant.instructions).toBe('instructions'); - expect(result.assistant.description).toBe('description'); - expect(result.assistant.metadata).toEqual({ key1: 'value1' }); + expect(result.assistant.model).toBe("gpt-4o"); + expect(result.assistant.name).toBe("name"); + expect(result.assistant.description).toBe("description"); + expect(result.assistant.instructions).toBe(null); + expect(result.assistant.metadata).toEqual({ + author: { + address: "0xabc123", + username: "username", + picture: "https://example.com/image.png", + }, + bio: "bio", + chatStarter: ["chat_starter_1", "chat_starter_2"], + greetingMessage: "hello", + image: "https://example.com/image.png", + tags: ["tag1", "tag2"], + }); }); it('should get assistant', async () => { const assistant = await ainft.assistant.get(objectId, tokenId, assistantId); - - expect(assistant.id).toBe(assistantId); - expect(assistant.model).toBe('gpt-4o-mini'); - expect(assistant.name).toBe('name'); - expect(assistant.instructions).toBe('instructions'); - expect(assistant.description).toBe('description'); - expect(assistant.metadata).toEqual({ key1: 'value1' }); }); it('should list assistants', async () => { @@ -56,23 +72,15 @@ describe.skip('assistant', () => { expect(result.items).toBeDefined(); }); - it('should update assistant', async () => { + it("should update assistant", async () => { const result = await ainft.assistant.update(objectId, tokenId, assistantId, { - model: 'gpt-4', - name: 'new_name', - instructions: 'new_instructions', - description: 'new_description', - metadata: { key1: 'value1', key2: 'value2' }, + name: "new_name", }); expect(result.tx_hash).toMatch(TX_HASH_REGEX); expect(result.result).toBeDefined(); expect(result.assistant.id).toBe(assistantId); - expect(result.assistant.model).toBe('gpt-4'); - expect(result.assistant.name).toBe('new_name'); - expect(result.assistant.instructions).toBe('new_instructions'); - expect(result.assistant.description).toBe('new_description'); - expect(result.assistant.metadata).toEqual({ key1: 'value1', key2: 'value2' }); + expect(result.assistant.name).toBe("new_name"); }); it('should delete assistant', async () => { From 8dde07d848bffd35e885ab7770b2b4011a2e4217 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Tue, 15 Oct 2024 11:17:18 +0900 Subject: [PATCH 02/15] test: skip test. --- test/ai/assistant.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ai/assistant.test.ts b/test/ai/assistant.test.ts index d6528401..f22a0fc1 100644 --- a/test/ai/assistant.test.ts +++ b/test/ai/assistant.test.ts @@ -2,7 +2,7 @@ import AinftJs from "../../src/ainft"; import { address, assistantId, objectId, privateKey, tokenId } from "../test_data"; import { ASSISTANT_REGEX, TX_HASH_REGEX } from "../constants"; -describe("assistant", () => { +describe.skip("assistant", () => { let ainft: AinftJs; beforeAll(async () => { From 003406f0b37303d32ed8d5a79d95397a0ca25d01 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Tue, 15 Oct 2024 14:53:30 +0900 Subject: [PATCH 03/15] fix: fix response data. --- src/ai/thread.ts | 70 +++++++++++++++++++++++++++++------------------ src/types.ts | 2 +- src/utils/util.ts | 1 + 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index 8844e174..efeb193f 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -50,8 +50,8 @@ export class Threads extends FactoryBase { await validateObject(this.ain, objectId); await validateToken(this.ain, objectId, tokenId); await validateAssistant(this.ain, objectId, tokenId); - const assistant = await getAssistant(this.ain, appId, tokenId); + const assistant = await getAssistant(this.ain, appId, tokenId); const serviceName = getServiceName(); await validateServerConfigurationForObject(this.ain, objectId, serviceName); @@ -59,15 +59,6 @@ export class Threads extends FactoryBase { const body = { objectId, tokenId, - assistant: { - id: assistant?.id, - model: assistant?.config?.model, - name: assistant?.config?.name, - instructions: assistant?.config?.instructions, - description: assistant?.config?.description || null, - metadata: assistant?.config?.metadata || null, - createdAt: assistant?.createdAt, - }, address, ...(metadata && !_.isEmpty(metadata) && { metadata }), }; @@ -78,10 +69,35 @@ export class Threads extends FactoryBase { data: body, }); + const thread = { + id: data.id, + metadata: data.metadata, + createdAt: data.createdAt, + assistant: { + id: assistant.id, + createdAt: assistant.createdAt, + model: assistant.model, + name: assistant.name, + description: assistant.description || null, + instructions: assistant.instructions || null, + metadata: { + author: assistant.metadata?.author || null, + bio: assistant.metadata?.bio || null, + chatStarter: assistant.metadata?.chatStarter + ? Object.values(assistant.metadata?.chatStarter) + : null, + greetingMessage: assistant.metadata?.greetingMessage || null, + image: assistant.metadata?.image || null, + tags: assistant.metadata?.tags ? Object.values(assistant.metadata?.tags) : null, + }, + metrics: assistant.metrics || {}, + }, + }; + const txBody = this.buildTxBodyForCreateThread(address, objectId, tokenId, data); const result = await sendTx(txBody, this.ain); - return { ...result, thread: data, tokenId }; + return { ...result, thread, tokenId }; } /** @@ -190,7 +206,7 @@ export class Threads extends FactoryBase { const thread = { id: data.id, metadata: data.metadata || {}, - created_at: data.createdAt, + createdAt: data.createdAt, }; return thread; @@ -219,7 +235,7 @@ export class Threads extends FactoryBase { const allThreads = await Promise.all( objectIds.map(async (objectId) => { const tokens = await this.fetchTokens(objectId); - return this.flattenThreads(objectId, tokens); + return await this.flattenThreads(objectId, tokens); }) ); const threads = allThreads.flat(); @@ -237,13 +253,13 @@ export class Threads extends FactoryBase { address: string, objectId: string, tokenId: string, - { id, metadata, created_at }: Thread + { id, metadata, createdAt }: Thread ) { const appId = AinftObject.getAppId(objectId); const threadPath = Path.app(appId).token(tokenId).ai().history(address).thread(id).value(); const value = { id, - createdAt: created_at, + createdAt, messages: true, ...(metadata && !_.isEmpty(metadata) && { metadata }), }; @@ -288,13 +304,13 @@ export class Threads extends FactoryBase { return this.ain.db.ref(tokensPath).getValue(); } - private flattenThreads(objectId: string, tokens: any) { + private async flattenThreads(objectId: string, tokens: any) { const flatten: any = []; - _.forEach(tokens, (token, tokenId) => { - const assistant = token.ai; - if (!assistant) { + await Promise.all(_.map(tokens, async (token, tokenId) => { + if (!token.ai) { return; } + const assistant = await getAssistant(this.ain, `ainft721_${objectId.toLowerCase()}`, tokenId); const histories = assistant.history; if (typeof histories !== 'object' || histories === true) { return; @@ -310,25 +326,27 @@ export class Threads extends FactoryBase { flatten.push({ id: thread.id, metadata: thread.metadata || {}, - created_at: thread.createdAt, - updated_at: updatedAt, + createdAt: thread.createdAt, + updatedAt, assistant: { id: assistant.id, + createdAt: assistant.createdAt, objectId, tokenId, - owner: token.owner, + tokenOwner: token.owner, model: assistant.config.model, name: assistant.config.name, - instructions: assistant.config.instructions, description: assistant.config.description || null, + instructions: assistant.config.instructions || null, metadata: assistant.config.metadata || {}, - created_at: assistant.createdAt, + metrics: assistant.metrics || {}, }, author: { address }, }); + }); }); - }); - }); + }) + ); return flatten; } diff --git a/src/types.ts b/src/types.ts index 78a4277d..42e2b2fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1360,7 +1360,7 @@ export interface Thread { */ metadata: object | {}; /** The UNIX timestamp in seconds. */ - created_at: number; + createdAt: number; } export interface ThreadDeleted { diff --git a/src/utils/util.ts b/src/utils/util.ts index 7d710d88..f087bc53 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -60,6 +60,7 @@ export const getAssistant = async (ain: Ain, appId: string, tokenId: string) => const assistantPath = Path.app(appId).token(tokenId).ai().value(); const assistant = await getValue(ain, assistantPath); if (!assistant) { return null } + // TODO(jiyoung): hide api endpoint. const response = await axios.get(`${AGENT_API_ENDPOINT[getEnv()]}/agents/${assistant.id}`); const data = response.data; From 75a39015fe71f9367cbbbb4b3539f65f829f9aee Mon Sep 17 00:00:00 2001 From: jiyoung Date: Tue, 15 Oct 2024 17:33:31 +0900 Subject: [PATCH 04/15] fix: fix list agents api. --- src/ai/assistant.ts | 32 +++++++++++++++++--------------- src/utils/util.ts | 4 +++- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/ai/assistant.ts b/src/ai/assistant.ts index 4b772e0a..fe95ddec 100644 --- a/src/ai/assistant.ts +++ b/src/ai/assistant.ts @@ -267,18 +267,7 @@ export class Assistants extends FactoryBase { name: _assistant.config.name, description: _assistant.config.description || null, instructions: _assistant.config.instructions || null, - metadata: { - author: _assistant.config.metadata?.author || null, - bio: _assistant.config.metadata?.bio || null, - chatStarter: _assistant.config.metadata?.chatStarter - ? Object.values(_assistant.config.metadata?.chatStarter) - : null, - greetingMessage: _assistant.config.metadata?.greetingMessage || null, - image: _assistant.config.metadata?.image || null, - tags: _assistant.config.metadata?.tags - ? Object.values(_assistant.config.metadata?.tags) - : null, - }, + metadata: _assistant.metadata || {}, metrics: _assistant.metrics || {}, }; @@ -508,15 +497,28 @@ export class Assistants extends FactoryBase { const tokens: NftTokens = (await this.ain.db.ref(tokensPath).getValue()) || {}; const assistants = await Promise.all( - Object.entries(tokens).map(async ([id, token]) => { + Object.entries(tokens).map(async ([tokenId, token]) => { if (!address || token.owner === address) { - return await getAssistant(this.ain, appId, id); + const assistant = await getAssistant(this.ain, appId, tokenId); + return { + id: assistant.id, + createdAt: assistant.createdAt, + objectId, + tokenId, + tokenOwner: token.owner, + model: assistant.config.model, + name: assistant.config.name, + description: assistant.config.description || null, + instructions: assistant.config.instructions || null, + metadata: assistant.metadata || {}, + metrics: assistant.metrics || {}, + }; } return null; }) ); - return assistants.filter((assistant) => assistant !== null); + return assistants.filter((assistant): assistant is Assistant => assistant !== null); } private sortAssistants(assistants: Assistant[], order: "asc" | "desc") { diff --git a/src/utils/util.ts b/src/utils/util.ts index f087bc53..5245a801 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -55,6 +55,7 @@ export const valueExists = async (ain: Ain, path: string): Promise => { }; export const getAssistant = async (ain: Ain, appId: string, tokenId: string) => { + // TODO(jiyoung): fix me. // TODO(jiyoung): fix circular reference with Ainft721Object.getAppId. // const appId = AinftObject.getAppId(objectId); const assistantPath = Path.app(appId).token(tokenId).ai().value(); @@ -63,7 +64,8 @@ export const getAssistant = async (ain: Ain, appId: string, tokenId: string) => // TODO(jiyoung): hide api endpoint. const response = await axios.get(`${AGENT_API_ENDPOINT[getEnv()]}/agents/${assistant.id}`); - const data = response.data; + const data = response.data?.data; + assistant.metadata = data?.metadata; assistant.metrics = { numCalls: data?.metrics?.num_calls || null, numThreads: data?.metrics?.num_threads || null, From fca387b10925f5040d03ac2a44eb325fc75c1e26 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Wed, 16 Oct 2024 10:27:29 +0900 Subject: [PATCH 05/15] fix: pass tokens without assistant. --- src/ai/assistant.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ai/assistant.ts b/src/ai/assistant.ts index fe95ddec..4073d87c 100644 --- a/src/ai/assistant.ts +++ b/src/ai/assistant.ts @@ -497,14 +497,17 @@ export class Assistants extends FactoryBase { const tokens: NftTokens = (await this.ain.db.ref(tokensPath).getValue()) || {}; const assistants = await Promise.all( - Object.entries(tokens).map(async ([tokenId, token]) => { + Object.entries(tokens).map(async ([id, token]) => { if (!address || token.owner === address) { - const assistant = await getAssistant(this.ain, appId, tokenId); + if (!token.ai) { + return null; + } + const assistant = await getAssistant(this.ain, appId, id); return { id: assistant.id, createdAt: assistant.createdAt, objectId, - tokenId, + tokenId: id, tokenOwner: token.owner, model: assistant.config.model, name: assistant.config.name, From 3130fa2432a1578593ee30a4fd711f23fbb04020 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Wed, 16 Oct 2024 10:48:31 +0900 Subject: [PATCH 06/15] fix: add money interface. --- src/types.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 42e2b2fe..878b792a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1299,6 +1299,11 @@ export interface AssistantMetadata extends Metadata { tags?: string[] | null; } +export interface Money { + amount: number; + unit: 'USD' | 'AIN'; +} + export interface AssistantMetrics { /** The number of API calls. */ numCalls?: number; @@ -1307,13 +1312,13 @@ export interface AssistantMetrics { /** The number of users. */ numUsers?: number; /** The total credits used by users. */ - totalUsedCredits?: number; + totalUsedCredits?: Money; /** The total revenue of the assistant. */ - totalRevenue?: number; + totalRevenue?: Money; /** The ad revenue of the assistant. */ - adRevenue?: number; + adRevenue?: Money; /** The platform revenue of the assistant. */ - platformRevenue?: number; + platformRevenue?: Money; } export interface AssistantDeleted { From 1e125be7d30a1309dcd94f0ddac729b74d6e5443 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Thu, 17 Oct 2024 10:17:40 +0900 Subject: [PATCH 07/15] fix: fix fetch logic. --- src/ai/assistant.ts | 55 ++++++++---------------------------------- src/ai/message.ts | 4 +-- src/ai/thread.ts | 20 +++++++-------- src/constants.ts | 2 +- src/utils/util.ts | 35 ++++++++++----------------- src/utils/validator.ts | 37 +++++++++++++++------------- 6 files changed, 56 insertions(+), 97 deletions(-) diff --git a/src/ai/assistant.ts b/src/ai/assistant.ts index 4073d87c..e5c0f80f 100644 --- a/src/ai/assistant.ts +++ b/src/ai/assistant.ts @@ -249,29 +249,7 @@ export class Assistants extends FactoryBase { * @returns A promise that resolves with the assistant. */ async get(objectId: string, tokenId: string, assistantId: string): Promise { - const appId = AinftObject.getAppId(objectId); - - await validateObject(this.ain, objectId); - await validateToken(this.ain, objectId, tokenId); - - const _assistant = await getAssistant(this.ain, appId, tokenId); - const _token = await getToken(this.ain, objectId, tokenId); - - const assistant: Assistant = { - id: _assistant.id, - createdAt: _assistant.createdAt, - objectId, - tokenId, - tokenOwner: _token.owner, - model: _assistant.config.model, - name: _assistant.config.name, - description: _assistant.config.description || null, - instructions: _assistant.config.instructions || null, - metadata: _assistant.metadata || {}, - metrics: _assistant.metrics || {}, - }; - - return assistant; + return getAssistant(this.ain, objectId, tokenId, assistantId); } /** @@ -494,34 +472,21 @@ export class Assistants extends FactoryBase { private async getAssistants(objectId: string, address?: string | null) { const appId = AinftObject.getAppId(objectId); const tokensPath = Path.app(appId).tokens().value(); - const tokens: NftTokens = (await this.ain.db.ref(tokensPath).getValue()) || {}; + const tokens: NftTokens = (await this.ain.db.ref().getValue(tokensPath)) || {}; const assistants = await Promise.all( - Object.entries(tokens).map(async ([id, token]) => { - if (!address || token.owner === address) { - if (!token.ai) { - return null; - } - const assistant = await getAssistant(this.ain, appId, id); - return { - id: assistant.id, - createdAt: assistant.createdAt, - objectId, - tokenId: id, - tokenOwner: token.owner, - model: assistant.config.model, - name: assistant.config.name, - description: assistant.config.description || null, - instructions: assistant.config.instructions || null, - metadata: assistant.metadata || {}, - metrics: assistant.metrics || {}, - }; + Object.entries(tokens).map(async ([tokenId, token]) => { + if (token.ai) { + const assistantId = token.ai.id; + return await getAssistant(this.ain, objectId, tokenId, assistantId); } - return null; }) ); - return assistants.filter((assistant): assistant is Assistant => assistant !== null); + return assistants.filter( + (assistant): assistant is Assistant => + assistant !== null && (!address || assistant.tokenOwner === address) + ); } private sortAssistants(assistants: Assistant[], order: "asc" | "desc") { diff --git a/src/ai/message.ts b/src/ai/message.ts index 0a1c858e..f17643f8 100644 --- a/src/ai/message.ts +++ b/src/ai/message.ts @@ -50,13 +50,13 @@ export class Messages extends FactoryBase { await validateObject(this.ain, objectId); await validateToken(this.ain, objectId, tokenId); - await validateAssistant(this.ain, objectId, tokenId); + const _assistant = await validateAssistant(this.ain, objectId, tokenId); await validateThread(this.ain, objectId, tokenId, address, threadId); const serviceName = getServiceName(); await validateServerConfigurationForObject(this.ain, objectId, serviceName); - const assistant = await getAssistant(this.ain, appId, tokenId); + const assistant = await getAssistant(this.ain, objectId, tokenId, _assistant.id); const newMessages = await this.sendMessage(serviceName, threadId, objectId, tokenId, assistant.id, address, body); const allMessages = await this.getAllMessages(appId, tokenId, address, threadId, newMessages); diff --git a/src/ai/thread.ts b/src/ai/thread.ts index efeb193f..5b8c45d6 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -44,14 +44,13 @@ export class Threads extends FactoryBase { tokenId: string, { metadata }: ThreadCreateParams ): Promise { - const appId = AinftObject.getAppId(objectId); const address = await this.ain.signer.getAddress(); await validateObject(this.ain, objectId); await validateToken(this.ain, objectId, tokenId); - await validateAssistant(this.ain, objectId, tokenId); + const _assistant = await validateAssistant(this.ain, objectId, tokenId); - const assistant = await getAssistant(this.ain, appId, tokenId); + const assistant = await getAssistant(this.ain, objectId, tokenId, _assistant.id); const serviceName = getServiceName(); await validateServerConfigurationForObject(this.ain, objectId, serviceName); @@ -310,8 +309,9 @@ export class Threads extends FactoryBase { if (!token.ai) { return; } - const assistant = await getAssistant(this.ain, `ainft721_${objectId.toLowerCase()}`, tokenId); - const histories = assistant.history; + const assistantId = token.ai.id; + const assistant = await getAssistant(this.ain, objectId, tokenId, assistantId); + const histories = token.ai.history; if (typeof histories !== 'object' || histories === true) { return; } @@ -334,11 +334,11 @@ export class Threads extends FactoryBase { objectId, tokenId, tokenOwner: token.owner, - model: assistant.config.model, - name: assistant.config.name, - description: assistant.config.description || null, - instructions: assistant.config.instructions || null, - metadata: assistant.config.metadata || {}, + model: assistant.model, + name: assistant.name, + description: assistant.description || null, + instructions: assistant.instructions || null, + metadata: assistant.metadata || {}, metrics: assistant.metrics || {}, }, author: { address }, diff --git a/src/constants.ts b/src/constants.ts index d99d118e..64cd0ca1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,6 +33,6 @@ export const MESSAGE_GC_NUM_SIBLINGS_DELETED = 10; export const DEFAULT_AINIZE_SERVICE_NAME = 'aina_backend'; export const WHITELISTED_OBJECT_IDS: Record = { - dev: ['0xCE3c4D8dA38c77dEC4ca99cD26B1C4BF116FC401'], + dev: ['0xA1425e477cF3e9413681d1508cF154C50f337675'], prod: ['0x6C8bB2aCBab0D807D74eB04034aA9Fd8c8E9C365'], }; diff --git a/src/utils/util.ts b/src/utils/util.ts index 5245a801..1f5c8df1 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -54,29 +54,20 @@ export const valueExists = async (ain: Ain, path: string): Promise => { return !!(await ain.db.ref(path).getValue()); }; -export const getAssistant = async (ain: Ain, appId: string, tokenId: string) => { - // TODO(jiyoung): fix me. - // TODO(jiyoung): fix circular reference with Ainft721Object.getAppId. - // const appId = AinftObject.getAppId(objectId); - const assistantPath = Path.app(appId).token(tokenId).ai().value(); - const assistant = await getValue(ain, assistantPath); - if (!assistant) { return null } - +export const getAssistant = async ( + ain: Ain, + objectId: string, + tokenId: string, + assistantId: string +) => { + const token = await getToken(ain, objectId, tokenId); // TODO(jiyoung): hide api endpoint. - const response = await axios.get(`${AGENT_API_ENDPOINT[getEnv()]}/agents/${assistant.id}`); - const data = response.data?.data; - assistant.metadata = data?.metadata; - assistant.metrics = { - numCalls: data?.metrics?.num_calls || null, - numThreads: data?.metrics?.num_threads || null, - numUsers: data?.metrics?.num_users || null, - totalUsedCredits: data?.metrics?.total_used_credits || null, - totalRevenue: data?.metrics?.total_revenue || null, - adRevenue: data?.metrics?.ad_revenue || null, - platformRevenue: data?.metrics?.platform_revenue || null, - }; - - return assistant; + const response = await axios.get(`${AGENT_API_ENDPOINT[getEnv()]}/agents/${assistantId}`); + const assistant = response.data?.data; + if (!assistant) { + return null; + } + return { ...assistant, tokenOwner: token.owner }; }; export const getToken = async (ain: Ain, objectId: string, tokenId: string) => { diff --git a/src/utils/validator.ts b/src/utils/validator.ts index 79519ffc..e5f0a024 100644 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -1,9 +1,9 @@ -import Ain from '@ainblockchain/ain-js'; -import AinftObject from '../ainft721Object'; -import { MessageMap } from '../types'; -import { Path } from './path'; -import { valueExists, getValue } from './util'; -import { AinftError } from '../error'; +import Ain from "@ainblockchain/ain-js"; +import AinftObject from "../ainft721Object"; +import { MessageMap } from "../types"; +import { Path } from "./path"; +import { valueExists, getValue } from "./util"; +import { AinftError } from "../error"; export const isObjectOwner = async (ain: Ain, objectId: string, address: string) => { const appId = AinftObject.getAppId(objectId); @@ -17,7 +17,7 @@ export const validateObject = async (ain: Ain, objectId: string) => { const objectPath = Path.app(appId).value(); const object = await getValue(ain, objectPath, { is_shallow: true }); if (!object) { - throw new AinftError('not-found', `object not found: ${objectId}`); + throw new AinftError("not-found", `object not found: ${objectId}`); } }; @@ -30,7 +30,7 @@ export const validateServerConfigurationForObject = async ( const configPath = Path.app(appId).ai(serviceName).value(); if (!(await valueExists(ain, configPath))) { throw new AinftError( - 'precondition-failed', + "precondition-failed", `service configuration is missing for ${objectId}.` ); } @@ -38,23 +38,25 @@ export const validateServerConfigurationForObject = async ( export const validateObjectOwner = async (ain: Ain, objectId: string, address: string) => { if (!isObjectOwner(ain, objectId, address)) { - throw new AinftError('permission-denied', `${address} do not have owner permission.`); + throw new AinftError("permission-denied", `${address} do not have owner permission.`); } }; export const validateToken = async (ain: Ain, objectId: string, tokenId: string) => { const appId = AinftObject.getAppId(objectId); const tokenPath = Path.app(appId).token(tokenId).value(); - if (!(await valueExists(ain, tokenPath))) { - throw new AinftError('not-found', `token not found: ${objectId}(${tokenId})`); + const token = await getValue(ain, tokenPath, { is_shallow: true }); + if (!token) { + throw new AinftError("not-found", `token not found: ${objectId}(${tokenId})`); } + return token; }; export const validateDuplicateAssistant = async (ain: Ain, objectId: string, tokenId: string) => { const appId = AinftObject.getAppId(objectId); const assistantPath = Path.app(appId).token(tokenId).ai().value(); if (await valueExists(ain, assistantPath)) { - throw new AinftError('already-exists', 'assistant already exists.'); + throw new AinftError("already-exists", "assistant already exists."); } }; @@ -66,13 +68,14 @@ export const validateAssistant = async ( ) => { const appId = AinftObject.getAppId(objectId); const assistantPath = Path.app(appId).token(tokenId).ai().value(); - const assistant = await getValue(ain, assistantPath); + const assistant = await getValue(ain, assistantPath, { is_shallow: true }); if (!assistant) { - throw new AinftError('not-found', `assistant not found: ${assistantId}`); + throw new AinftError("not-found", `assistant not found: ${assistantId}`); } if (assistantId && assistantId !== assistant.id) { - throw new AinftError('bad-request', `invalid assistant id: ${assistantId} != ${assistant.id}`); + throw new AinftError("bad-request", `invalid assistant id: ${assistantId} != ${assistant.id}`); } + return assistant; }; export const validateThread = async ( @@ -85,7 +88,7 @@ export const validateThread = async ( const appId = AinftObject.getAppId(objectId); const threadPath = Path.app(appId).token(tokenId).ai().history(address).thread(threadId).value(); if (!(await valueExists(ain, threadPath))) { - throw new AinftError('not-found', `thread not found: ${threadId}`); + throw new AinftError("not-found", `thread not found: ${threadId}`); } }; @@ -112,5 +115,5 @@ export const validateMessage = async ( return; } } - throw new AinftError('not-found', `message not found: ${threadId}(${messageId})`); + throw new AinftError("not-found", `message not found: ${threadId}(${messageId})`); }; From dd045f515f05b09e663f153e86203328904303cd Mon Sep 17 00:00:00 2001 From: jiyoung Date: Thu, 17 Oct 2024 11:26:19 +0900 Subject: [PATCH 08/15] fix: return null. --- src/ai/assistant.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ai/assistant.ts b/src/ai/assistant.ts index e5c0f80f..79302ab4 100644 --- a/src/ai/assistant.ts +++ b/src/ai/assistant.ts @@ -480,6 +480,7 @@ export class Assistants extends FactoryBase { const assistantId = token.ai.id; return await getAssistant(this.ain, objectId, tokenId, assistantId); } + return null; }) ); From 1797cbf36a175918b0f498406f93d42663b2df31 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Thu, 17 Oct 2024 14:51:57 +0900 Subject: [PATCH 09/15] fix: fix typo. --- src/ai/thread.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index 5b8c45d6..7b8c1a0a 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -362,9 +362,9 @@ export class Threads extends FactoryBase { private sortThreads(threads: any, sort: string, order: 'asc' | 'desc') { if (sort === 'created') { - return _.orderBy(threads, ['created_at'], [order]); + return _.orderBy(threads, ['createdAt'], [order]); } else if (sort === 'updated') { - return _.orderBy(threads, ['updated_at'], [order]); + return _.orderBy(threads, ['updatedAt'], [order]); } else { throw new AinftError('bad-request', `invalid sort criteria: ${sort}`); } From a40a7b6b2816236df4465476b9df55560a030e25 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Thu, 17 Oct 2024 15:55:20 +0900 Subject: [PATCH 10/15] fix: add missing field. --- src/ai/thread.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index 7b8c1a0a..0c1a5b01 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -72,6 +72,7 @@ export class Threads extends FactoryBase { id: data.id, metadata: data.metadata, createdAt: data.createdAt, + updatedAt: data.createdAt, assistant: { id: assistant.id, createdAt: assistant.createdAt, From 3e782c9ef58ce587bff2528239bbf6eb3ed7982a Mon Sep 17 00:00:00 2001 From: jiyoung Date: Tue, 22 Oct 2024 11:33:23 +0900 Subject: [PATCH 11/15] fix: pass assistant id when creating thread. --- src/ai/thread.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index 0c1a5b01..5a3cebb4 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -58,6 +58,7 @@ export class Threads extends FactoryBase { const body = { objectId, tokenId, + assistantId: assistant.id, address, ...(metadata && !_.isEmpty(metadata) && { metadata }), }; From b65b86d8b5ac8e3e08fcf3d676f26ac8a7ca9780 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Wed, 23 Oct 2024 13:37:41 +0900 Subject: [PATCH 12/15] fix: convert milliseconds to seconds. --- src/ai/thread.ts | 95 ++++++++++++++++++++++++----------------------- src/utils/util.ts | 16 +++++++- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index 5a3cebb4..f13e461c 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -15,7 +15,7 @@ import { } from '../types'; import { Path } from '../utils/path'; import { buildSetValueOp, buildSetTxBody, sendTx } from '../utils/transaction'; -import { getAssistant, getValue } from '../utils/util'; +import { getAssistant, getValue, isMillisecond, toSecond } from '../utils/util'; import { validateAssistant, validateObject, @@ -203,14 +203,13 @@ export class Threads extends FactoryBase { .history(address) .thread(threadId) .value(); - const data = await this.ain.db.ref(threadPath).getValue(); - const thread = { - id: data.id, - metadata: data.metadata || {}, - createdAt: data.createdAt, - }; + const thread = await this.ain.db.ref(threadPath).getValue(); - return thread; + return { + id: thread.id, + createdAt: isMillisecond(thread.createdAt) ? toSecond(thread.createdAt) : thread.createdAt, + metadata: thread.metadata || {}, + }; } /** @@ -307,44 +306,48 @@ export class Threads extends FactoryBase { private async flattenThreads(objectId: string, tokens: any) { const flatten: any = []; - await Promise.all(_.map(tokens, async (token, tokenId) => { - if (!token.ai) { - return; - } - const assistantId = token.ai.id; - const assistant = await getAssistant(this.ain, objectId, tokenId, assistantId); - const histories = token.ai.history; - if (typeof histories !== 'object' || histories === true) { - return; - } - _.forEach(histories, (history, address) => { - const threads = _.get(history, 'threads'); - _.forEach(threads, (thread) => { - let updatedAt = thread.createdAt; - if (typeof thread.messages === 'object' && thread.messages !== null) { - const keys = Object.keys(thread.messages); - updatedAt = Number(keys[keys.length - 1]); - } - flatten.push({ - id: thread.id, - metadata: thread.metadata || {}, - createdAt: thread.createdAt, - updatedAt, - assistant: { - id: assistant.id, - createdAt: assistant.createdAt, - objectId, - tokenId, - tokenOwner: token.owner, - model: assistant.model, - name: assistant.name, - description: assistant.description || null, - instructions: assistant.instructions || null, - metadata: assistant.metadata || {}, - metrics: assistant.metrics || {}, - }, - author: { address }, - }); + await Promise.all( + _.map(tokens, async (token, tokenId) => { + if (!token.ai) { + return; + } + const assistantId = token.ai.id; + const assistant = await getAssistant(this.ain, objectId, tokenId, assistantId); + const histories = token.ai.history; + if (typeof histories !== 'object' || histories === true) { + return; + } + _.forEach(histories, (history, address) => { + const threads = _.get(history, 'threads'); + _.forEach(threads, (thread) => { + const createdAt = isMillisecond(thread.createdAt) + ? toSecond(thread.createdAt) + : thread.createdAt; + let updatedAt = createdAt; + if (typeof thread.messages === 'object' && thread.messages !== null) { + const keys = Object.keys(thread.messages); + updatedAt = Number(keys[keys.length - 1]); + } + flatten.push({ + id: thread.id, + metadata: thread.metadata || {}, + createdAt: createdAt, + updatedAt, + assistant: { + id: assistant.id, + createdAt: assistant.createdAt, + objectId, + tokenId, + tokenOwner: token.owner, + model: assistant.model, + name: assistant.name, + description: assistant.description || null, + instructions: assistant.instructions || null, + metadata: assistant.metadata || {}, + metrics: assistant.metrics || {}, + }, + author: { address }, + }); }); }); }) diff --git a/src/utils/util.ts b/src/utils/util.ts index 1f5c8df1..04b5f23e 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -67,7 +67,13 @@ export const getAssistant = async ( if (!assistant) { return null; } - return { ...assistant, tokenOwner: token.owner }; + return { + ...assistant, + createdAt: isMillisecond(assistant.createdAt) + ? toSecond(assistant.createdAt) + : assistant.createdAt, + tokenOwner: token.owner, + }; }; export const getToken = async (ain: Ain, objectId: string, tokenId: string) => { @@ -99,3 +105,11 @@ export const arrayToObject = (array: T[]): { [key: string]: T } => { }); return result; }; + +export const isMillisecond = (timestamp: number) => { + return timestamp.toString().length === 13; +}; + +export const toSecond = (millisecond: number) => { + return Math.floor(millisecond / 1000); +}; From 02b9a4466f8cbdaca61a0c2bd471da52ff9d9878 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Wed, 23 Oct 2024 14:21:41 +0900 Subject: [PATCH 13/15] refactor: replace ternary operator with function. --- src/ai/thread.ts | 8 +++----- src/utils/util.ts | 8 +++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index f13e461c..ab98f413 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -15,7 +15,7 @@ import { } from '../types'; import { Path } from '../utils/path'; import { buildSetValueOp, buildSetTxBody, sendTx } from '../utils/transaction'; -import { getAssistant, getValue, isMillisecond, toSecond } from '../utils/util'; +import { getAssistant, getValue, normalizeTimestamp } from '../utils/util'; import { validateAssistant, validateObject, @@ -207,7 +207,7 @@ export class Threads extends FactoryBase { return { id: thread.id, - createdAt: isMillisecond(thread.createdAt) ? toSecond(thread.createdAt) : thread.createdAt, + createdAt: normalizeTimestamp(thread.createdAt), metadata: thread.metadata || {}, }; } @@ -320,9 +320,7 @@ export class Threads extends FactoryBase { _.forEach(histories, (history, address) => { const threads = _.get(history, 'threads'); _.forEach(threads, (thread) => { - const createdAt = isMillisecond(thread.createdAt) - ? toSecond(thread.createdAt) - : thread.createdAt; + const createdAt = normalizeTimestamp(thread.createdAt); let updatedAt = createdAt; if (typeof thread.messages === 'object' && thread.messages !== null) { const keys = Object.keys(thread.messages); diff --git a/src/utils/util.ts b/src/utils/util.ts index 04b5f23e..c51a5178 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -69,9 +69,7 @@ export const getAssistant = async ( } return { ...assistant, - createdAt: isMillisecond(assistant.createdAt) - ? toSecond(assistant.createdAt) - : assistant.createdAt, + createdAt: normalizeTimestamp(assistant.createdAt), tokenOwner: token.owner, }; }; @@ -106,6 +104,10 @@ export const arrayToObject = (array: T[]): { [key: string]: T } => { return result; }; +export const normalizeTimestamp = (timestamp: number) => { + return isMillisecond(timestamp) ? toSecond(timestamp) : timestamp; +}; + export const isMillisecond = (timestamp: number) => { return timestamp.toString().length === 13; }; From 14b3ac6ade4c261fd3e0c706184f0fd4c655f7b8 Mon Sep 17 00:00:00 2001 From: jiyoung Date: Fri, 25 Oct 2024 10:10:55 +0900 Subject: [PATCH 14/15] fix: add missing token owner field. --- src/ai/thread.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ai/thread.ts b/src/ai/thread.ts index ab98f413..5dc79fdd 100644 --- a/src/ai/thread.ts +++ b/src/ai/thread.ts @@ -47,7 +47,7 @@ export class Threads extends FactoryBase { const address = await this.ain.signer.getAddress(); await validateObject(this.ain, objectId); - await validateToken(this.ain, objectId, tokenId); + const token = await validateToken(this.ain, objectId, tokenId); const _assistant = await validateAssistant(this.ain, objectId, tokenId); const assistant = await getAssistant(this.ain, objectId, tokenId, _assistant.id); @@ -77,6 +77,9 @@ export class Threads extends FactoryBase { assistant: { id: assistant.id, createdAt: assistant.createdAt, + objectId: objectId, + tokenId: tokenId, + tokenOwner: token.owner, model: assistant.model, name: assistant.name, description: assistant.description || null, From 400b0c7e6d4e018a658740ce5bbef066e85d6c7b Mon Sep 17 00:00:00 2001 From: jiyoung-an Date: Tue, 29 Oct 2024 01:43:10 +0000 Subject: [PATCH 15/15] Upgrade version to 2.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8546dfd8..f0559c66 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@ainft-team/ainft-js", "main": "dist/ainft.js", "types": "dist/ainft.d.ts", - "version": "2.1.2", + "version": "2.1.3", "engines": { "node": ">=16" },