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.
		
		
		
		
		
			
		
			
				
					
					
						
							124 lines
						
					
					
						
							4.1 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							124 lines
						
					
					
						
							4.1 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'; | |
| 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<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 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<typeof import('tablemark')>; | |
|     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'); | |
|   } | |
| }
 | |
| 
 |