Browse Source

Extend import of activities with Ghostfolio AI

pull/8128/head
Thomas Kaul 4 days ago
parent
commit
555152255c
  1. 12
      apps/api/src/app/endpoints/ai/ai.controller.spec.ts
  2. 6
      apps/api/src/app/endpoints/ai/ai.controller.ts
  3. 4
      apps/api/src/app/endpoints/ai/ai.schemas.spec.ts
  4. 4
      apps/api/src/app/endpoints/ai/ai.schemas.ts
  5. 6
      apps/api/src/app/endpoints/ai/ai.service.ts
  6. 12
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  7. 14
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss
  8. 1
      libs/common/src/lib/config.ts

12
apps/api/src/app/endpoints/ai/ai.controller.spec.ts

@ -4,7 +4,7 @@ import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.gu
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import {
MCP_MAX_ACTIVITIES,
AI_MAX_ACTIVITIES,
THROTTLE_AI_ACTIVITIES_LIMIT,
THROTTLE_DAILY_TTL
} from '@ghostfolio/common/config';
@ -132,23 +132,23 @@ describe('AiController', () => {
expect(aiService.getActivitiesFromFile).not.toHaveBeenCalled();
});
it(`Refuses a CSV file with more than ${MCP_MAX_ACTIVITIES} rows before a model call`, async () => {
it(`Refuses a CSV file with more than ${AI_MAX_ACTIVITIES} rows before a model call`, async () => {
await expect(
aiController.getActivitiesFromFile(
createResponse(),
createCsvFile(MCP_MAX_ACTIVITIES + 1)
createCsvFile(AI_MAX_ACTIVITIES + 1)
)
).rejects.toThrow(HttpException);
expect(aiService.getActivitiesFromFile).not.toHaveBeenCalled();
expect(Logger.prototype.warn).toHaveBeenCalledWith(
`Rejected a CSV file with ${MCP_MAX_ACTIVITIES + 1} rows`
`Rejected a CSV file with ${AI_MAX_ACTIVITIES + 1} rows`
);
});
it(`Accepts a CSV file with ${MCP_MAX_ACTIVITIES} rows`, async () => {
const file = createCsvFile(MCP_MAX_ACTIVITIES);
it(`Accepts a CSV file with ${AI_MAX_ACTIVITIES} rows`, async () => {
const file = createCsvFile(AI_MAX_ACTIVITIES);
await aiController.getActivitiesFromFile(createResponse(), file);

6
apps/api/src/app/endpoints/ai/ai.controller.ts

@ -10,7 +10,7 @@ import { ApiService } from '@ghostfolio/api/services/api/api.service';
import {
AI_ACTIVITIES_FILE_MAXIMUM_SIZE,
AI_ACTIVITIES_TIMEOUT,
MCP_MAX_ACTIVITIES,
AI_MAX_ACTIVITIES,
THROTTLE_AI_ACTIVITIES_LIMIT,
THROTTLE_DAILY_TTL
} from '@ghostfolio/common/config';
@ -116,14 +116,14 @@ export class AiController {
if (mediaType === 'text/csv') {
const dataRowCount = getDataRowCountOfCsvFile(file.buffer);
if (dataRowCount > MCP_MAX_ACTIVITIES) {
if (dataRowCount > AI_MAX_ACTIVITIES) {
this.logger.warn(`Rejected a CSV file with ${dataRowCount} rows`);
throw new HttpException(
{
error: getReasonPhrase(StatusCodes.BAD_REQUEST),
message: [
`The CSV file has more than ${MCP_MAX_ACTIVITIES} rows of activities`
`The CSV file has more than ${AI_MAX_ACTIVITIES} rows of activities`
]
},
StatusCodes.BAD_REQUEST

4
apps/api/src/app/endpoints/ai/ai.schemas.spec.ts

@ -1,5 +1,5 @@
import {
MCP_MAX_ACTIVITIES,
AI_MAX_ACTIVITIES,
SEARCH_QUERY_MAXIMUM_LENGTH
} from '@ghostfolio/common/config';
@ -44,7 +44,7 @@ describe('ACTIVITY_ROWS_OUTPUT', () => {
name: 'A'.repeat(SEARCH_QUERY_MAXIMUM_LENGTH + 1)
}),
createActivityRow({ fee: -1, quantity: -1, unitPrice: -1 }),
...Array.from({ length: MCP_MAX_ACTIVITIES }, () => {
...Array.from({ length: AI_MAX_ACTIVITIES }, () => {
return createActivityRow();
})
]

4
apps/api/src/app/endpoints/ai/ai.schemas.ts

@ -1,4 +1,4 @@
import { MCP_MAX_ACTIVITIES } from '@ghostfolio/common/config';
import { AI_MAX_ACTIVITIES } from '@ghostfolio/common/config';
import { Type as ActivityType } from '@prisma/client';
import { z } from 'zod';
@ -39,7 +39,7 @@ export const ACTIVITY_ROWS_OUTPUT = z.object({
unitPrice: z.number().describe('The unit price of the activity')
})
)
.describe(`The activities of the file, at most ${MCP_MAX_ACTIVITIES}`)
.describe(`The activities of the file, at most ${AI_MAX_ACTIVITIES}`)
});
export const ROW_MATCHES_OUTPUT = z.object({

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

@ -9,8 +9,8 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { PortfolioTableService } from '@ghostfolio/api/services/portfolio-table/portfolio-table.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
AI_MAX_ACTIVITIES,
DEFAULT_LOCALE,
MCP_MAX_ACTIVITIES,
NON_INVESTMENT_ACTIVITY_TYPES,
PROPERTY_API_KEY_OPENROUTER,
PROPERTY_OPENROUTER_MODEL,
@ -299,7 +299,7 @@ export class AiService {
'For an interest or a liability, give 1 as quantity and the amount as unit price.',
'For a fee which is not part of a buy or a sell, give 0 as quantity, 0 as unit price and the amount as fee.',
`If the format of a date or of a number is ambiguous and the file gives no other hint, read it as in the locale ${locale}.`,
`Give at most ${MCP_MAX_ACTIVITIES} rows.`
`Give at most ${AI_MAX_ACTIVITIES} rows.`
].join('\n'),
maxOutputTokens: AiService.VISION_MAXIMUM_OUTPUT_TOKEN_COUNT,
messages: [{ content, role: 'user' }],
@ -333,7 +333,7 @@ export class AiService {
`Read ${output.rows.length} rows from a file in ${this.getSecondsSince(startTime)} seconds with ${totalUsage.totalTokens} tokens for ${this.getCost(steps)} USD`
);
return output.rows.slice(0, MCP_MAX_ACTIVITIES);
return output.rows.slice(0, AI_MAX_ACTIVITIES);
}
public async getPrompt({

12
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -2,8 +2,8 @@ import { GfFileDropDirective } from '@ghostfolio/client/directives/file-drop/fil
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import {
AI_ACTIVITIES_FILE_MAXIMUM_SIZE,
DEFAULT_DATE_RANGE,
MCP_MAX_ACTIVITIES
AI_MAX_ACTIVITIES,
DEFAULT_DATE_RANGE
} from '@ghostfolio/common/config';
import {
CreateAccountWithBalancesDto,
@ -133,7 +133,7 @@ export class GfImportActivitiesDialogComponent {
protected isGhostfolioAiEnabled = false;
protected isLoading = false;
protected isMaxActivitiesOfGhostfolioAiReached = false;
protected readonly maxActivitiesOfGhostfolioAi = MCP_MAX_ACTIVITIES;
protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES;
protected mode: 'DIVIDEND';
protected pageIndex = 0;
protected readonly pageSize = 8;
@ -387,9 +387,9 @@ export class GfImportActivitiesDialogComponent {
if (
fileExtension === 'csv' &&
this.importActivitiesService.getDataRowCountOfCsv(await file.text()) >
MCP_MAX_ACTIVITIES
AI_MAX_ACTIVITIES
) {
return $localize`The CSV file has more than ${MCP_MAX_ACTIVITIES}:maxActivities: rows of activities`;
return $localize`The CSV file has more than ${AI_MAX_ACTIVITIES}:maxActivities: rows of activities`;
}
return undefined;
@ -587,7 +587,7 @@ export class GfImportActivitiesDialogComponent {
activitiesOfFile = activities;
this.assetProfiles = assetProfiles;
this.isMaxActivitiesOfGhostfolioAiReached =
activities.length + rowsWithoutMatch.length >= MCP_MAX_ACTIVITIES;
activities.length + rowsWithoutMatch.length >= AI_MAX_ACTIVITIES;
this.rowsWithoutMatch = rowsWithoutMatch;
return from(

14
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.scss

@ -43,13 +43,6 @@
font-size: 2.5rem;
}
.mat-mdc-form-field {
.mat-mdc-progress-spinner {
right: 1.5rem;
top: calc(50% - 10px);
}
}
.mat-mdc-progress-spinner {
@include mat.progress-spinner-overrides(
(
@ -61,6 +54,13 @@
}
}
.mat-mdc-form-field {
.mat-mdc-progress-spinner {
right: 1.5rem;
top: calc(50% - 10px);
}
}
.mat-expansion-panel {
@include mat.expansion-overrides(
(

1
libs/common/src/lib/config.ts

@ -40,6 +40,7 @@ export const warnColorRgb = {
export const AI_ACTIVITIES_FILE_MAXIMUM_SIZE = 10 * 1024 * 1024;
export const AI_ACTIVITIES_TIMEOUT = ms('2 minutes');
export const AI_MAX_ACTIVITIES = 25;
export const ASSET_CLASS_MAPPING = new Map<AssetClass, AssetSubClass[]>([
[AssetClass.ALTERNATIVE_INVESTMENT, [AssetSubClass.COLLECTIBLE]],

Loading…
Cancel
Save