|
|
@ -10,26 +10,25 @@ import type { AiPromptMode } from '@ghostfolio/common/types'; |
|
|
import { Injectable } from '@nestjs/common'; |
|
|
import { Injectable } from '@nestjs/common'; |
|
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider'; |
|
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider'; |
|
|
import { generateText } from 'ai'; |
|
|
import { generateText } from 'ai'; |
|
|
import { ColumnDescriptor } from 'tablemark'; |
|
|
import type { ColumnDescriptor } from 'tablemark'; |
|
|
|
|
|
|
|
|
const tablemark = require('tablemark').default; |
|
|
@Injectable() |
|
|
|
|
|
export class AiService { |
|
|
// Column definitions for holdings table
|
|
|
private readonly HOLDINGS_TABLE_COLUMNS: ({ |
|
|
const HOLDINGS_TABLE_COLUMNS: ({ key: string } & ColumnDescriptor)[] = [ |
|
|
key: string; |
|
|
{ key: 'CURRENCY', name: 'Currency' }, |
|
|
} & ColumnDescriptor)[] = [ |
|
|
{ key: 'NAME', name: 'Name' }, |
|
|
{ key: 'NAME', name: 'Name' }, |
|
|
{ key: 'SYMBOL', name: 'Symbol' }, |
|
|
{ key: 'SYMBOL', name: 'Symbol' }, |
|
|
|
|
|
{ key: 'CURRENCY', name: 'Currency' }, |
|
|
{ key: 'ASSET_CLASS', name: 'Asset Class' }, |
|
|
{ key: 'ASSET_CLASS', name: 'Asset Class' }, |
|
|
|
|
|
{ key: 'ASSET_SUB_CLASS', name: 'Asset Sub Class' }, |
|
|
{ |
|
|
{ |
|
|
align: 'right', |
|
|
align: 'right', |
|
|
key: 'ALLOCATION_PERCENTAGE', |
|
|
key: 'ALLOCATION_PERCENTAGE', |
|
|
name: 'Allocation in Percentage' |
|
|
name: 'Allocation in Percentage' |
|
|
}, |
|
|
} |
|
|
{ key: 'ASSET_SUB_CLASS', name: 'Asset Sub Class' } |
|
|
|
|
|
]; |
|
|
]; |
|
|
|
|
|
|
|
|
@Injectable() |
|
|
|
|
|
export class AiService { |
|
|
|
|
|
public constructor( |
|
|
public constructor( |
|
|
private readonly portfolioService: PortfolioService, |
|
|
private readonly portfolioService: PortfolioService, |
|
|
private readonly propertyService: PropertyService |
|
|
private readonly propertyService: PropertyService |
|
|
@ -75,50 +74,71 @@ export class AiService { |
|
|
userId |
|
|
userId |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const holdingsTableColumns: ColumnDescriptor[] = HOLDINGS_TABLE_COLUMNS.map( |
|
|
const holdingsTableColumns: ColumnDescriptor[] = |
|
|
({ align, name }) => { |
|
|
this.HOLDINGS_TABLE_COLUMNS.map(({ align, name }) => { |
|
|
return { name, align: align ?? 'left' }; |
|
|
return { name, align: align ?? 'left' }; |
|
|
} |
|
|
}); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const holdingsTableRows = Object.values(holdings) |
|
|
const holdingsTableRows = Object.values(holdings) |
|
|
.sort((a, b) => { |
|
|
.sort((a, b) => { |
|
|
return b.allocationInPercentage - a.allocationInPercentage; |
|
|
return b.allocationInPercentage - a.allocationInPercentage; |
|
|
}) |
|
|
}) |
|
|
.map((holding) => { |
|
|
.map( |
|
|
return HOLDINGS_TABLE_COLUMNS.reduce( |
|
|
({ |
|
|
|
|
|
allocationInPercentage, |
|
|
|
|
|
assetClass, |
|
|
|
|
|
assetSubClass, |
|
|
|
|
|
currency, |
|
|
|
|
|
name: label, |
|
|
|
|
|
symbol |
|
|
|
|
|
}) => { |
|
|
|
|
|
return this.HOLDINGS_TABLE_COLUMNS.reduce( |
|
|
(row, { key, name }) => { |
|
|
(row, { key, name }) => { |
|
|
switch (key) { |
|
|
switch (key) { |
|
|
case 'CURRENCY': |
|
|
case 'ALLOCATION_PERCENTAGE': |
|
|
row[name] = holding.currency; |
|
|
row[name] = `${(allocationInPercentage * 100).toFixed(3)}%`; |
|
|
break; |
|
|
break; |
|
|
case 'NAME': |
|
|
|
|
|
row[name] = holding.name; |
|
|
case 'ASSET_CLASS': |
|
|
|
|
|
row[name] = assetClass ?? ''; |
|
|
break; |
|
|
break; |
|
|
case 'SYMBOL': |
|
|
|
|
|
row[name] = holding.symbol; |
|
|
case 'ASSET_SUB_CLASS': |
|
|
|
|
|
row[name] = assetSubClass ?? ''; |
|
|
break; |
|
|
break; |
|
|
case 'ASSET_CLASS': |
|
|
|
|
|
row[name] = holding.assetClass ?? ''; |
|
|
case 'CURRENCY': |
|
|
|
|
|
row[name] = currency; |
|
|
break; |
|
|
break; |
|
|
case 'ALLOCATION_PERCENTAGE': |
|
|
|
|
|
row[name] = |
|
|
case 'NAME': |
|
|
`${(holding.allocationInPercentage * 100).toFixed(3)}%`; |
|
|
row[name] = label; |
|
|
break; |
|
|
break; |
|
|
case 'ASSET_SUB_CLASS': |
|
|
|
|
|
row[name] = holding.assetSubClass ?? ''; |
|
|
case 'SYMBOL': |
|
|
|
|
|
row[name] = symbol; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
default: |
|
|
default: |
|
|
row[name] = ''; |
|
|
row[name] = ''; |
|
|
break; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return row; |
|
|
return row; |
|
|
}, |
|
|
}, |
|
|
{} as Record<string, string> |
|
|
{} as Record<string, string> |
|
|
); |
|
|
); |
|
|
}); |
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Dynamic import to load ESM module from CommonJS context
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
|
|
|
|
const dynamicImport = new Function('s', 'return import(s)') as ( |
|
|
|
|
|
s: string |
|
|
|
|
|
) => Promise<typeof import('tablemark')>; |
|
|
|
|
|
const { tablemark } = await dynamicImport('tablemark'); |
|
|
|
|
|
|
|
|
const holdingsTableString = tablemark.default(holdingsTableRows, { |
|
|
const holdingsTableString = tablemark(holdingsTableRows, { |
|
|
columns: holdingsTableColumns |
|
|
columns: holdingsTableColumns |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|