mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
100 lines
3.4 KiB
100 lines
3.4 KiB
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
|
import {
|
|
PROPERTY_API_KEY_OPENROUTER,
|
|
PROPERTY_OPENROUTER_MODEL
|
|
} from '@ghostfolio/common/config';
|
|
import { Filter } from '@ghostfolio/common/interfaces';
|
|
import type { AiPromptMode } from '@ghostfolio/common/types';
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
import { generateText } from 'ai';
|
|
|
|
@Injectable()
|
|
export class AiService {
|
|
public constructor(
|
|
private readonly portfolioService: PortfolioService,
|
|
private readonly propertyService: PropertyService
|
|
) {}
|
|
|
|
public async generateText({ prompt }: { prompt: string }) {
|
|
const openRouterApiKey = await this.propertyService.getByKey<string>(
|
|
PROPERTY_API_KEY_OPENROUTER
|
|
);
|
|
|
|
const openRouterModel = await this.propertyService.getByKey<string>(
|
|
PROPERTY_OPENROUTER_MODEL
|
|
);
|
|
|
|
const openRouterService = createOpenRouter({
|
|
apiKey: openRouterApiKey
|
|
});
|
|
|
|
return generateText({
|
|
prompt,
|
|
model: openRouterService.chat(openRouterModel)
|
|
});
|
|
}
|
|
|
|
public async getPrompt({
|
|
filters,
|
|
impersonationId,
|
|
languageCode,
|
|
mode,
|
|
userCurrency,
|
|
userId
|
|
}: {
|
|
filters?: Filter[];
|
|
impersonationId: string;
|
|
languageCode: string;
|
|
mode: AiPromptMode;
|
|
userCurrency: string;
|
|
userId: string;
|
|
}) {
|
|
const { holdings } = await this.portfolioService.getDetails({
|
|
filters,
|
|
impersonationId,
|
|
userId
|
|
});
|
|
|
|
const holdingsTable = [
|
|
'| Name | Symbol | Currency | Asset Class | Asset Sub Class | Allocation in Percentage |',
|
|
'| --- | --- | --- | --- | --- | --- |',
|
|
...Object.values(holdings)
|
|
.sort((a, b) => {
|
|
return b.allocationInPercentage - a.allocationInPercentage;
|
|
})
|
|
.map(
|
|
({
|
|
allocationInPercentage,
|
|
assetClass,
|
|
assetSubClass,
|
|
currency,
|
|
name,
|
|
symbol
|
|
}) => {
|
|
return `| ${name} | ${symbol} | ${currency} | ${assetClass} | ${assetSubClass} | ${(allocationInPercentage * 100).toFixed(3)}% |`;
|
|
}
|
|
)
|
|
];
|
|
|
|
if (mode === 'portfolio') {
|
|
return holdingsTable.join('\n');
|
|
}
|
|
|
|
return [
|
|
`You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`,
|
|
...holdingsTable,
|
|
'Structure your answer with these sections:',
|
|
'Overview: Briefly summarize the portfolio’s composition and allocation rationale.',
|
|
'Risk Assessment: Identify potential risks, including market volatility, concentration, and sectoral imbalances.',
|
|
'Advantages: Highlight strengths, focusing on growth potential, diversification, or other benefits.',
|
|
'Disadvantages: Point out weaknesses, such as overexposure or lack of defensive assets.',
|
|
'Target Group: Discuss who this portfolio might suit (e.g., risk tolerance, investment goals, life stages, and experience levels).',
|
|
'Optimization Ideas: Offer ideas to complement the portfolio, ensuring they are constructive and neutral in tone.',
|
|
'Conclusion: Provide a concise summary highlighting key insights.',
|
|
`Provide your answer in the following language: ${languageCode}.`
|
|
].join('\n');
|
|
}
|
|
}
|
|
|