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_VISION
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { isValidCustomAssetProfileSymbol } from '@ghostfolio/common/helper';
import type { UserWithSettings } from '@ghostfolio/common/types';
@ -100,8 +99,7 @@ describe('AiService', () => {
dataProviderService = {
getDataSources: jest
.fn()
.mockResolvedValue([DataSource.MANUAL, DataSource.YAHOO]),
isPremiumRequired: jest.fn().mockReturnValue(false)
.mockResolvedValue([DataSource.MANUAL, DataSource.YAHOO])
} as unknown as DataProviderService;
mcpService = {
@ -357,8 +355,7 @@ describe('AiService', () => {
const user = {
accounts: [{ currency: 'CHF', id: 'account-id', name: 'Brokerage' }],
id: 'user-id',
settings: { settings: { baseCurrency: 'EUR', locale: 'de-CH' } },
subscription: { type: SubscriptionType.Basic }
settings: { settings: { baseCurrency: 'EUR', locale: 'de-CH' } }
} 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 () => {
@ -402,7 +399,6 @@ describe('AiService', () => {
rows,
accounts: user.accounts,
broker: 'Swissquote',
subscription: user.subscription,
userCurrency: 'EUR',
userId: 'user-id'
});
@ -975,55 +971,6 @@ describe('AiService', () => {
],
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 () => {

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

@ -124,7 +124,6 @@ export class AiService {
broker,
rows,
accounts: user.accounts,
subscription: user.subscription,
userCurrency: user.settings.settings.baseCurrency,
userId: user.id
});
@ -152,7 +151,6 @@ export class AiService {
accounts,
broker,
rows,
subscription,
userCurrency,
userId
}: {
@ -160,7 +158,6 @@ export class AiService {
accounts: AccountToMatch[];
broker?: string;
rows: ActivityRow[];
subscription?: UserWithSettings['subscription'];
userCurrency: string;
userId: string;
}) {
@ -211,7 +208,6 @@ export class AiService {
abortSignal,
accountHints,
assets,
subscription,
userId
})
: { accounts: [], assets: [] };
@ -524,15 +520,9 @@ export class AiService {
/**
* 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
* import refuses for the user
* import refuses
*/
private async getHoldingsToMatch({
subscription,
userId
}: {
subscription?: UserWithSettings['subscription'];
userId: string;
}) {
private async getHoldingsToMatch({ userId }: { userId: string }) {
const [{ content }, dataSources] = await Promise.all([
this.mcpService.getPortfolio({ userId, withValues: false }),
this.dataProviderService.getDataSources()
@ -540,11 +530,7 @@ export class AiService {
return this.getHoldingsFromTable(content[0].text).filter(
({ dataSource }) => {
return this.isValidDataSourceForImport({
dataSource,
dataSources,
subscription
});
return this.isValidDataSourceForImport({ dataSource, dataSources });
}
);
}
@ -608,8 +594,8 @@ export class AiService {
* 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
* 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
* asset profile of the search instead and the whole import does not fail.
* is not given to the model, so that the model selects an asset profile of
* the search instead and the whole import does not fail.
* 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,
* the model gets no holdings. The reasoning is disabled, as it can add
@ -625,13 +611,11 @@ export class AiService {
abortSignal,
accountHints,
assets,
subscription,
userId
}: {
abortSignal?: AbortSignal;
accountHints: AccountHint[];
assets: AssetToMatch[];
subscription?: UserWithSettings['subscription'];
userId: string;
}) {
const startTime = performance.now();
@ -650,7 +634,7 @@ export class AiService {
})
: undefined,
assets.length > 0
? this.getHoldingsToMatch({ subscription, userId }).catch(
? this.getHoldingsToMatch({ userId }).catch(
(error: Error): undefined => {
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
* source for the user: it is configured and does not require Ghostfolio
* Premium, for example after the Premium subscription has expired. The data
* source is first transformed as POST api/v1/import transforms it. A symbol
* which the data source cannot resolve, for example a delisted one, is not
* found, as this needs a request to the data provider.
* source of a holding: it is configured. The data source is first
* transformed as POST api/v1/import transforms it. A data source which
* requires Ghostfolio Premium is taken, as the import takes it for an asset
* profile which the user already has. A symbol which the data source cannot
* resolve, for example a delisted one, is not found, as this needs a
* request to the data provider.
*/
private isValidDataSourceForImport({
dataSource,
dataSources,
subscription
dataSources
}: {
dataSource: string;
dataSources: DataSource[];
subscription?: UserWithSettings['subscription'];
}) {
const dataSourceOfImport = transformDataSourceInRequest({
dataSource,
configurationService: this.configurationService
});
return (
dataSources.includes(dataSourceOfImport) &&
!this.dataProviderService.isPremiumRequired({
subscription,
dataSource: dataSourceOfImport
return dataSources.includes(
transformDataSourceInRequest({
dataSource,
configurationService: this.configurationService
})
);
}

Loading…
Cancel
Save