Browse Source

Remove Premium check from matching of assets

pull/8196/head
Thomas Kaul 1 day ago
parent
commit
727cb4ba5f
  1. 57
      apps/api/src/app/endpoints/ai/ai.service.spec.ts
  2. 57
      apps/api/src/app/endpoints/ai/ai.service.ts

57
apps/api/src/app/endpoints/ai/ai.service.spec.ts

@ -12,7 +12,6 @@ import {
PROPERTY_OPENROUTER_MODEL_AGENT, PROPERTY_OPENROUTER_MODEL_AGENT,
PROPERTY_OPENROUTER_MODEL_VISION PROPERTY_OPENROUTER_MODEL_VISION
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper'; import { isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper';
import type { UserWithSettings } from '@ghostfolio/common/types'; import type { UserWithSettings } from '@ghostfolio/common/types';
@ -100,8 +99,7 @@ describe('AiService', () => {
dataProviderService = { dataProviderService = {
getDataSources: jest getDataSources: jest
.fn() .fn()
.mockResolvedValue([DataSource.MANUAL, DataSource.YAHOO]), .mockResolvedValue([DataSource.MANUAL, DataSource.YAHOO])
isPremiumRequired: jest.fn().mockReturnValue(false)
} as unknown as DataProviderService; } as unknown as DataProviderService;
mcpService = { mcpService = {
@ -357,8 +355,7 @@ describe('AiService', () => {
const user = { const user = {
accounts: [{ currency: 'CHF', id: 'account-id', name: 'Brokerage' }], accounts: [{ currency: 'CHF', id: 'account-id', name: 'Brokerage' }],
id: 'user-id', id: 'user-id',
settings: { settings: { baseCurrency: 'EUR', locale: 'de-CH' } }, settings: { settings: { baseCurrency: 'EUR', locale: 'de-CH' } }
subscription: { type: SubscriptionType.Basic }
} as unknown as UserWithSettings; } as unknown as UserWithSettings;
it('Reads the rows of the file and matches them with the accounts, the base currency and the id of the user', async () => { it('Reads the rows of the file and matches them with the accounts, the base currency and the id of the user', async () => {
@ -402,7 +399,6 @@ describe('AiService', () => {
rows, rows,
accounts: user.accounts, accounts: user.accounts,
broker: 'Swissquote', broker: 'Swissquote',
subscription: user.subscription,
userCurrency: 'EUR', userCurrency: 'EUR',
userId: 'user-id' userId: 'user-id'
}); });
@ -975,55 +971,6 @@ describe('AiService', () => {
], ],
rowsWithoutMatch: [] rowsWithoutMatch: []
}); });
expect(dataProviderService.isPremiumRequired).toHaveBeenCalledWith({
dataSource: DataSource.YAHOO,
subscription: undefined
});
});
it('Passes no holding with a data source which requires Premium to the model and gives a row whose asset match is such a holding as a row without a match', async () => {
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
jest.mocked(dataProviderService.isPremiumRequired).mockReturnValue(true);
const row = createActivityRow({
accountName: null,
isin: 'IE00BK5BQT80',
symbol: null
});
const subscription = {
type: SubscriptionType.Basic
} as UserWithSettings['subscription'];
mockRowMatches({
assetProfilesOfSearch: [],
assets: [
{ currency: 'USD', dataSource: 'YAHOO', id: 0, symbol: 'VWRA.SW' }
]
});
mockHoldings();
await expect(
aiService.getActivitiesFromRows({
accounts,
subscription,
userCurrency,
userId,
rows: [row]
})
).resolves.toEqual({ activities: [], rowsWithoutMatch: [row] });
expect(dataProviderService.isPremiumRequired).toHaveBeenCalledWith({
subscription,
dataSource: DataSource.YAHOO
});
const { prompt } = jest.mocked(generateText).mock.calls[0][0];
expect(JSON.parse(prompt as string)).toHaveProperty('holdings', []);
}); });
it('Passes no holding with a data source which is not configured to the model and gives a row whose asset match is such a holding as a row without a match', async () => { it('Passes no holding with a data source which is not configured to the model and gives a row whose asset match is such a holding as a row without a match', async () => {

57
apps/api/src/app/endpoints/ai/ai.service.ts

@ -124,7 +124,6 @@ export class AiService {
broker, broker,
rows, rows,
accounts: user.accounts, accounts: user.accounts,
subscription: user.subscription,
userCurrency: user.settings.settings.baseCurrency, userCurrency: user.settings.settings.baseCurrency,
userId: user.id userId: user.id
}); });
@ -152,7 +151,6 @@ export class AiService {
accounts, accounts,
broker, broker,
rows, rows,
subscription,
userCurrency, userCurrency,
userId userId
}: { }: {
@ -160,7 +158,6 @@ export class AiService {
accounts: AccountToMatch[]; accounts: AccountToMatch[];
broker?: string; broker?: string;
rows: ActivityRow[]; rows: ActivityRow[];
subscription?: UserWithSettings['subscription'];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}) { }) {
@ -211,7 +208,6 @@ export class AiService {
abortSignal, abortSignal,
accountHints, accountHints,
assets, assets,
subscription,
userId userId
}) })
: { accounts: [], assets: [] }; : { accounts: [], assets: [] };
@ -524,15 +520,9 @@ export class AiService {
/** /**
* Gives the holdings of the user with the tool get-portfolio of the server * Gives the holdings of the user with the tool get-portfolio of the server
* of the Model Context Protocol, without the holdings whose data source the * of the Model Context Protocol, without the holdings whose data source the
* import refuses for the user * import refuses
*/ */
private async getHoldingsToMatch({ private async getHoldingsToMatch({ userId }: { userId: string }) {
subscription,
userId
}: {
subscription?: UserWithSettings['subscription'];
userId: string;
}) {
const [{ content }, dataSources] = await Promise.all([ const [{ content }, dataSources] = await Promise.all([
this.mcpService.getPortfolio({ userId, withValues: false }), this.mcpService.getPortfolio({ userId, withValues: false }),
this.dataProviderService.getDataSources() this.dataProviderService.getDataSources()
@ -540,11 +530,7 @@ export class AiService {
return this.getHoldingsFromTable(content[0].text).filter( return this.getHoldingsFromTable(content[0].text).filter(
({ dataSource }) => { ({ dataSource }) => {
return this.isValidDataSourceForImport({ return this.isValidDataSourceForImport({ dataSource, dataSources });
dataSource,
dataSources,
subscription
});
} }
); );
} }
@ -608,8 +594,8 @@ export class AiService {
* symbol only. The holdings let the model select an asset profile which the * symbol only. The holdings let the model select an asset profile which the
* user already has, also if no search gives it, for example as no data * user already has, also if no search gives it, for example as no data
* provider finds its ISIN. A holding whose data source the import refuses * provider finds its ISIN. A holding whose data source the import refuses
* for the user is not given to the model, so that the model selects an * is not given to the model, so that the model selects an asset profile of
* asset profile of the search instead and the whole import does not fail. * the search instead and the whole import does not fail.
* If get-accounts fails, the model gets no accounts, so that only the * If get-accounts fails, the model gets no accounts, so that only the
* account hints have no match. If get-portfolio or the data sources fail, * account hints have no match. If get-portfolio or the data sources fail,
* the model gets no holdings. The reasoning is disabled, as it can add * the model gets no holdings. The reasoning is disabled, as it can add
@ -625,13 +611,11 @@ export class AiService {
abortSignal, abortSignal,
accountHints, accountHints,
assets, assets,
subscription,
userId userId
}: { }: {
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
accountHints: AccountHint[]; accountHints: AccountHint[];
assets: AssetToMatch[]; assets: AssetToMatch[];
subscription?: UserWithSettings['subscription'];
userId: string; userId: string;
}) { }) {
const startTime = performance.now(); const startTime = performance.now();
@ -650,7 +634,7 @@ export class AiService {
}) })
: undefined, : undefined,
assets.length > 0 assets.length > 0
? this.getHoldingsToMatch({ subscription, userId }).catch( ? this.getHoldingsToMatch({ userId }).catch(
(error: Error): undefined => { (error: Error): undefined => {
this.logger.warn(`Could not get the holdings: ${error.message}`); this.logger.warn(`Could not get the holdings: ${error.message}`);
@ -751,31 +735,24 @@ export class AiService {
/** /**
* Checks without a request to a data provider if the import takes a data * Checks without a request to a data provider if the import takes a data
* source for the user: it is configured and does not require Ghostfolio * source of a holding: it is configured. The data source is first
* Premium, for example after the Premium subscription has expired. The data * transformed as POST api/v1/import transforms it. A data source which
* source is first transformed as POST api/v1/import transforms it. A symbol * requires Ghostfolio Premium is taken, as the import takes it for an asset
* which the data source cannot resolve, for example a delisted one, is not * profile which the user already has. A symbol which the data source cannot
* found, as this needs a request to the data provider. * resolve, for example a delisted one, is not found, as this needs a
* request to the data provider.
*/ */
private isValidDataSourceForImport({ private isValidDataSourceForImport({
dataSource, dataSource,
dataSources, dataSources
subscription
}: { }: {
dataSource: string; dataSource: string;
dataSources: DataSource[]; dataSources: DataSource[];
subscription?: UserWithSettings['subscription'];
}) { }) {
const dataSourceOfImport = transformDataSourceInRequest({ return dataSources.includes(
dataSource, transformDataSourceInRequest({
configurationService: this.configurationService dataSource,
}); configurationService: this.configurationService
return (
dataSources.includes(dataSourceOfImport) &&
!this.dataProviderService.isPremiumRequired({
subscription,
dataSource: dataSourceOfImport
}) })
); );
} }

Loading…
Cancel
Save