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'; import type { ColumnDescriptor } from 'tablemark'; @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( PROPERTY_API_KEY_OPENROUTER ); const openRouterModel = await this.propertyService.getByKey( 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 holdingsTableColumns: ColumnDescriptor[] = [ { name: 'Name' }, { name: 'Symbol' }, { name: 'Currency' }, { name: 'Asset Class' }, { name: 'Asset Sub Class' }, { align: 'right', name: 'Allocation in Percentage' } ]; const holdingsTableRows = Object.values(holdings) .sort((a, b) => { return b.allocationInPercentage - a.allocationInPercentage; }) .map( ({ allocationInPercentage, assetClass, assetSubClass, currency, name, symbol }) => { return { Name: name, Symbol: symbol, Currency: currency, 'Asset Class': assetClass ?? '', 'Asset Sub Class': assetSubClass ?? '', 'Allocation in Percentage': `${(allocationInPercentage * 100).toFixed(3)}%` }; } ); // Dynamic import to load ESM module from CommonJS context // eslint-disable-next-line @typescript-eslint/no-implied-eval const dynamicImport = new Function('s', 'return import(s)') as ( s: string ) => Promise; const { tablemark } = await dynamicImport('tablemark'); const holdingsTableString = tablemark(holdingsTableRows, { columns: holdingsTableColumns }); if (mode === 'portfolio') { return holdingsTableString; } return [ `You are a neutral financial assistant. Please analyze the following investment portfolio (base currency being ${userCurrency}) in simple words.`, holdingsTableString, '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'); } }