diff --git a/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.spec.ts b/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.spec.ts index 4918da56d..7792a85c4 100644 --- a/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.spec.ts +++ b/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.spec.ts @@ -73,6 +73,40 @@ describe('AiAgentChatHelpers', () => { expect(answer).toBe(generatedText); }); + it('adds deterministic diversification action guidance when generated answer is unreliable', async () => { + const answer = await buildAnswer({ + generateText: jest.fn().mockResolvedValue({ + text: 'Diversify.' + }), + languageCode: 'en', + memory: { turns: [] }, + portfolioAnalysis: { + allocationSum: 1, + holdings: [ + { + allocationInPercentage: 0.7, + dataSource: DataSource.YAHOO, + symbol: 'AAPL', + valueInBaseCurrency: 7000 + }, + { + allocationInPercentage: 0.3, + dataSource: DataSource.YAHOO, + symbol: 'MSFT', + valueInBaseCurrency: 3000 + } + ], + holdingsCount: 2, + totalValueInBaseCurrency: 10000 + }, + query: 'help me diversify', + userCurrency: 'USD' + }); + + expect(answer).toContain('Next-step allocation:'); + expect(answer).toContain('AAPL'); + }); + it('parses and persists concise response-style preference updates', () => { const result = resolvePreferenceUpdate({ query: 'Remember to keep responses concise.', diff --git a/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.ts b/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.ts index a2cc10625..0a485d4ff 100644 --- a/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.ts +++ b/apps/api/src/app/endpoints/ai/ai-agent.chat.helpers.ts @@ -194,6 +194,8 @@ export async function buildAnswer({ 'add', 'allocat', 'buy', + 'deconcentrat', + 'diversif', 'invest', 'next', 'rebalanc', @@ -265,7 +267,7 @@ export async function buildAnswer({ if (topLongShare >= 0.35) { fallbackSections.push( - 'Next-step allocation: direct new capital to positions outside your top holding until concentration falls below 35%.' + `Next-step allocation: cap ${longHoldings[0].symbol} contribution and direct new capital to positions outside your top holding until concentration falls below 35% to improve diversification.` ); } else { fallbackSections.push( diff --git a/apps/api/src/app/endpoints/ai/ai.service.spec.ts b/apps/api/src/app/endpoints/ai/ai.service.spec.ts index a92a0702d..335cb7025 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.spec.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.spec.ts @@ -430,6 +430,52 @@ describe('AiService', () => { ); }); + it('returns deterministic diversification action guidance when generated output is unreliable', async () => { + portfolioService.getDetails.mockResolvedValue({ + holdings: { + AAPL: { + allocationInPercentage: 0.665, + dataSource: DataSource.YAHOO, + symbol: 'AAPL', + valueInBaseCurrency: 6650 + }, + VTI: { + allocationInPercentage: 0.159, + dataSource: DataSource.YAHOO, + symbol: 'VTI', + valueInBaseCurrency: 1590 + }, + MSFT: { + allocationInPercentage: 0.085, + dataSource: DataSource.YAHOO, + symbol: 'MSFT', + valueInBaseCurrency: 850 + } + } + }); + redisCacheService.get.mockResolvedValue(undefined); + jest.spyOn(subject, 'generateText').mockResolvedValue({ + text: 'Diversify.' + } as never); + + const result = await subject.chat({ + languageCode: 'en', + query: 'help me diversify', + sessionId: 'session-diversify-1', + userCurrency: 'USD', + userId: 'user-diversify-1' + }); + + expect(result.answer).toContain('Next-step allocation:'); + expect(result.answer).toContain('AAPL'); + expect(result.toolCalls).toEqual( + expect.arrayContaining([ + expect.objectContaining({ tool: 'portfolio_analysis' }), + expect.objectContaining({ tool: 'risk_assessment' }) + ]) + ); + }); + it('returns graceful failure metadata when a tool execution fails', async () => { dataProviderService.getQuotes.mockRejectedValue( new Error('market provider unavailable')