Browse Source

fix(ai): route portfolio-value queries to portfolio analysis

pull/6395/head
Max P 1 month ago
parent
commit
aa35ca2783
  1. 4
      apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.ts
  2. 8
      apps/api/src/app/endpoints/ai/ai-agent.utils.spec.ts
  3. 18
      apps/api/src/app/endpoints/ai/ai-agent.utils.ts
  4. 40
      apps/api/src/app/endpoints/ai/ai.service.spec.ts

4
apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.ts

@ -414,6 +414,10 @@ export async function buildAnswer({
return sum + valueInBaseCurrency; return sum + valueInBaseCurrency;
}, 0); }, 0);
fallbackSections.push(
`Total portfolio value: ${portfolioAnalysis.totalValueInBaseCurrency.toFixed(2)} ${userCurrency} across ${portfolioAnalysis.holdingsCount} holdings.`
);
if (totalLongValue > 0) { if (totalLongValue > 0) {
const topLongHoldingsSummary = longHoldings const topLongHoldingsSummary = longHoldings
.slice(0, 3) .slice(0, 3)

8
apps/api/src/app/endpoints/ai/ai-agent.utils.spec.ts

@ -48,6 +48,14 @@ describe('AiAgentUtils', () => {
).toEqual(['market_data_lookup']); ).toEqual(['market_data_lookup']);
}); });
it('selects portfolio analysis for portfolio value query wording', () => {
expect(
determineToolPlan({
query: 'how much money i have?'
})
).toEqual(['portfolio_analysis']);
});
it('returns no tools when no clear tool keyword exists', () => { it('returns no tools when no clear tool keyword exists', () => {
expect( expect(
determineToolPlan({ determineToolPlan({

18
apps/api/src/app/endpoints/ai/ai-agent.utils.ts

@ -49,11 +49,17 @@ const REBALANCE_KEYWORDS = [
]; ];
const STRESS_TEST_KEYWORDS = ['crash', 'drawdown', 'shock', 'stress']; const STRESS_TEST_KEYWORDS = ['crash', 'drawdown', 'shock', 'stress'];
const PORTFOLIO_VALUE_QUERY_PATTERNS = [
/\bhow much(?:\s+\w+){0,4}\s+(?:money|cash|value|worth)\b.*\b(?:i|my)\b.*\b(?:have|own)\b/,
/\b(?:net\s+worth|portfolio\s+value|portfolio\s+worth|account\s+balance|total\s+portfolio\s+value)\b/
];
const ANSWER_NUMERIC_INTENT_KEYWORDS = [ const ANSWER_NUMERIC_INTENT_KEYWORDS = [
'allocat', 'allocat',
'balance',
'drawdown', 'drawdown',
'hhi', 'hhi',
'market', 'market',
'money',
'performance', 'performance',
'price', 'price',
'quote', 'quote',
@ -61,7 +67,8 @@ const ANSWER_NUMERIC_INTENT_KEYWORDS = [
'risk', 'risk',
'shock', 'shock',
'stress', 'stress',
'trim' 'trim',
'worth'
]; ];
const ANSWER_ACTIONABLE_KEYWORDS = [ const ANSWER_ACTIONABLE_KEYWORDS = [
'add', 'add',
@ -275,6 +282,11 @@ export function determineToolPlan({
const hasStressTestIntent = STRESS_TEST_KEYWORDS.some((keyword) => { const hasStressTestIntent = STRESS_TEST_KEYWORDS.some((keyword) => {
return normalizedQuery.includes(keyword); return normalizedQuery.includes(keyword);
}); });
const hasPortfolioValueIntent = PORTFOLIO_VALUE_QUERY_PATTERNS.some(
(pattern) => {
return pattern.test(normalizedQuery);
}
);
if ( if (
normalizedQuery.includes('portfolio') || normalizedQuery.includes('portfolio') ||
@ -286,6 +298,10 @@ export function determineToolPlan({
selectedTools.add('portfolio_analysis'); selectedTools.add('portfolio_analysis');
} }
if (hasPortfolioValueIntent) {
selectedTools.add('portfolio_analysis');
}
if ( if (
normalizedQuery.includes('risk') || normalizedQuery.includes('risk') ||
normalizedQuery.includes('concentration') || normalizedQuery.includes('concentration') ||

40
apps/api/src/app/endpoints/ai/ai.service.spec.ts

@ -291,6 +291,46 @@ describe('AiService', () => {
expect(generateTextSpy).not.toHaveBeenCalled(); expect(generateTextSpy).not.toHaveBeenCalled();
}); });
it('uses portfolio data for "how much money i have?" queries', async () => {
portfolioService.getDetails.mockResolvedValue({
holdings: {
AAPL: {
allocationInPercentage: 0.6,
dataSource: DataSource.YAHOO,
symbol: 'AAPL',
valueInBaseCurrency: 6000
},
MSFT: {
allocationInPercentage: 0.4,
dataSource: DataSource.YAHOO,
symbol: 'MSFT',
valueInBaseCurrency: 4000
}
}
});
redisCacheService.get.mockResolvedValue(undefined);
jest.spyOn(subject, 'generateText').mockRejectedValue(new Error('offline'));
const result = await subject.chat({
languageCode: 'en',
query: 'how much money i have?',
sessionId: 'session-total-value',
userCurrency: 'USD',
userId: 'user-total-value'
});
expect(result.answer).toContain('Total portfolio value: 10000.00 USD');
expect(result.answer).not.toContain('I am Ghostfolio AI');
expect(result.toolCalls).toEqual(
expect.arrayContaining([
expect.objectContaining({
status: 'success',
tool: 'portfolio_analysis'
})
])
);
});
it('routes ambiguous action follow-up query through recommendation tools when finance memory exists', async () => { it('routes ambiguous action follow-up query through recommendation tools when finance memory exists', async () => {
portfolioService.getDetails.mockResolvedValue({ portfolioService.getDetails.mockResolvedValue({
holdings: { holdings: {

Loading…
Cancel
Save