Browse Source

Task/improve consistency of Ghostfolio AI (#8298)

* Set temperature of models of Ghostfolio AI to 0

* Update changelog
pull/8297/head^2
Thomas Kaul 4 days ago
committed by GitHub
parent
commit
4f17f0ecd4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 11
      apps/api/src/app/endpoints/ai/ai.service.spec.ts
  3. 12
      apps/api/src/app/endpoints/ai/ai.service.ts

6
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/), 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). 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 ## 3.92.0 - 2026-10-24
### Changed ### Changed

11
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({ await aiService.getActivityRowsFromFile({
file: { buffer: Buffer.from('%PDF-1.7'), mediaType: 'application/pdf' }, file: { buffer: Buffer.from('%PDF-1.7'), mediaType: 'application/pdf' },
locale: 'de-CH' locale: 'de-CH'
@ -214,6 +214,10 @@ describe('AiService', () => {
reasoning: { effort: 'none' }, reasoning: { effort: 'none' },
usage: { include: true } usage: { include: true }
}); });
expect(generateText).toHaveBeenCalledWith(
expect.objectContaining({ temperature: 0 })
);
}); });
it('Falls back to the general model if no vision model is set', async () => { 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({}); mockRowMatches({});
await aiService.getActivitiesFromRows({ await aiService.getActivitiesFromRows({
@ -1545,7 +1549,8 @@ describe('AiService', () => {
expect.objectContaining({ expect.objectContaining({
abortSignal, abortSignal,
model, model,
output: { schema: ROW_MATCHES_OUTPUT } output: { schema: ROW_MATCHES_OUTPUT },
temperature: 0
}) })
); );

12
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 * 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 * 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 * 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 * 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 * 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. * at all, hence the finish reason is checked as well.
@ -343,7 +344,8 @@ export class AiService {
usage: { include: true } usage: { include: true }
} }
}), }),
output: Output.object({ schema: ACTIVITY_ROWS_OUTPUT }) output: Output.object({ schema: ACTIVITY_ROWS_OUTPUT }),
temperature: 0
}).catch((error: unknown) => { }).catch((error: unknown) => {
if ( if (
NoObjectGeneratedError.isInstance(error) && NoObjectGeneratedError.isInstance(error) &&
@ -551,7 +553,8 @@ export class AiService {
* the model gets no accounts, so that only the account hints have no match. * 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 * 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 * 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 * 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 * 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 * 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) => { assets: assets.map((asset, index) => {
return { ...asset, assetProfiles: searches[index].assetProfiles }; return { ...asset, assetProfiles: searches[index].assetProfiles };
}) })
}) }),
temperature: 0
}); });
const assetMatches = output.assets.filter(({ dataSource, id, symbol }) => { const assetMatches = output.assets.filter(({ dataSource, id, symbol }) => {

Loading…
Cancel
Save