From 4f17f0ecd4a357b860f835ab11c4d0034c20d2d0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:43:02 +0200 Subject: [PATCH] Task/improve consistency of Ghostfolio AI (#8298) * Set temperature of models of Ghostfolio AI to 0 * Update changelog --- CHANGELOG.md | 6 ++++++ apps/api/src/app/endpoints/ai/ai.service.spec.ts | 11 ++++++++--- apps/api/src/app/endpoints/ai/ai.service.ts | 12 ++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc979dd76..525f5fd1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the consistency of the import of activities with _Ghostfolio AI_ (experimental) + ## 3.92.0 - 2026-10-24 ### Changed diff --git a/apps/api/src/app/endpoints/ai/ai.service.spec.ts b/apps/api/src/app/endpoints/ai/ai.service.spec.ts index 1634415536..01d59cab75 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.spec.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.spec.ts @@ -202,7 +202,7 @@ describe('AiService', () => { ); }); - it('Restricts the routing to endpoints with zero data retention, disables the reasoning and passes a PDF file to the model itself', async () => { + it('Restricts the routing to endpoints with zero data retention, disables the reasoning, sets the temperature to 0 and passes a PDF file to the model itself', async () => { await aiService.getActivityRowsFromFile({ file: { buffer: Buffer.from('%PDF-1.7'), mediaType: 'application/pdf' }, locale: 'de-CH' @@ -214,6 +214,10 @@ describe('AiService', () => { reasoning: { effort: 'none' }, usage: { include: true } }); + + expect(generateText).toHaveBeenCalledWith( + expect.objectContaining({ temperature: 0 }) + ); }); it('Falls back to the general model if no vision model is set', async () => { @@ -1524,7 +1528,7 @@ describe('AiService', () => { ); }); - it('Restricts the routing to endpoints with zero data retention, disables the reasoning and gives the model no tools', async () => { + it('Restricts the routing to endpoints with zero data retention, disables the reasoning, sets the temperature to 0 and gives the model no tools', async () => { mockRowMatches({}); await aiService.getActivitiesFromRows({ @@ -1545,7 +1549,8 @@ describe('AiService', () => { expect.objectContaining({ abortSignal, model, - output: { schema: ROW_MATCHES_OUTPUT } + output: { schema: ROW_MATCHES_OUTPUT }, + temperature: 0 }) ); diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts index 3c7965688b..44ba23c0cf 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.ts @@ -288,7 +288,8 @@ export class AiService { * the rows still have to be matched to asset profiles and accounts. The * name of the file is passed as well, as it can name the broker. The * reasoning is disabled, as it can add thousands of output tokens even for a - * single row. A row above the maximum number of activities is left out. An + * single row. The temperature is 0, so that the same file gives the same + * rows. A row above the maximum number of activities is left out. An * output which reaches the maximum number of output tokens is cut, hence it * gives an error for the caller. A cut output without any text is not parsed * at all, hence the finish reason is checked as well. @@ -343,7 +344,8 @@ export class AiService { usage: { include: true } } }), - output: Output.object({ schema: ACTIVITY_ROWS_OUTPUT }) + output: Output.object({ schema: ACTIVITY_ROWS_OUTPUT }), + temperature: 0 }).catch((error: unknown) => { if ( NoObjectGeneratedError.isInstance(error) && @@ -551,7 +553,8 @@ export class AiService { * the model gets no accounts, so that only the account hints have no match. * If the holdings or the data sources cannot be got, the model gets no * holdings. The reasoning is disabled, as it can add thousands of output - * tokens. An asset match is left out if neither the search of its own asset + * tokens. The temperature is 0, so that the same assets give the same + * matches. An asset match is left out if neither the search of its own asset * nor the holdings gave its asset profile, or if its holding has another ISIN * than its own asset, as the import refuses a symbol which the data source * cannot resolve and as the model can mix up the ids of the assets. The ISINs @@ -632,7 +635,8 @@ export class AiService { assets: assets.map((asset, index) => { return { ...asset, assetProfiles: searches[index].assetProfiles }; }) - }) + }), + temperature: 0 }); const assetMatches = output.assets.filter(({ dataSource, id, symbol }) => {