openstock/lib/actions/stock-analysis.actions.ts

164 lines
5.0 KiB
TypeScript

'use server';
import { unstable_cache } from 'next/cache';
import { callAIProviderWithFallback, hasConfiguredAIProvider } from '@/lib/ai-provider';
import { getStockSentimentInsights } from './adanos.actions';
import { getBasicFinancials, getCompanyNews, getCompanyProfile, getQuote } from './finnhub.actions';
import {
buildStockAnalysisPrompt,
parseStockAnalysisResponse,
type StockAIAnalysis,
} from './stock-analysis.helpers';
import type { StockSentimentInsights } from './adanos.helpers';
export interface StockDetailInsights {
sentimentInsights: StockSentimentInsights | null;
aiAnalysis: StockAIAnalysis | null;
}
function normalizeHeadline(article: MarketNewsArticle) {
return {
headline: article.headline,
summary: article.summary,
source: article.source,
datetime: article.datetime,
};
}
async function generateStockDetailInsights(normalizedSymbol: string): Promise<StockDetailInsights> {
const sentimentPromise = getStockSentimentInsights(normalizedSymbol);
const [quote, profile, basicFinancials, news, sentimentInsights] = await Promise.all([
getQuote(normalizedSymbol),
getCompanyProfile(normalizedSymbol),
getBasicFinancials(normalizedSymbol),
getCompanyNews(normalizedSymbol),
sentimentPromise,
]);
if (!hasConfiguredAIProvider()) {
return {
sentimentInsights,
aiAnalysis: null,
};
}
const hasAnalysisContext =
Boolean(quote) ||
Boolean(profile) ||
Boolean(sentimentInsights) ||
Boolean(basicFinancials?.metric && Object.keys(basicFinancials.metric).length > 0) ||
news.length > 0;
if (!hasAnalysisContext) {
return {
sentimentInsights,
aiAnalysis: null,
};
}
try {
const response = await callAIProviderWithFallback(
buildStockAnalysisPrompt({
symbol: normalizedSymbol,
quote,
profile,
basicFinancials,
sentimentInsights,
recentHeadlines: news.slice(0, 4).map(normalizeHeadline),
}),
);
return {
sentimentInsights,
aiAnalysis: parseStockAnalysisResponse(
response,
normalizedSymbol,
profile?.name ?? sentimentInsights?.companyName ?? null,
),
};
} catch (error) {
console.error(`Failed to generate AI stock analysis for ${normalizedSymbol}`, error);
return {
sentimentInsights,
aiAnalysis: null,
};
}
}
export async function getStockDetailInsights(symbol: string): Promise<StockDetailInsights> {
const normalizedSymbol = symbol.trim().toUpperCase();
if (!normalizedSymbol) {
return {
sentimentInsights: null,
aiAnalysis: null,
};
}
return unstable_cache(() => generateStockDetailInsights(normalizedSymbol), ['stock-detail-insights', normalizedSymbol], {
revalidate: 900,
tags: [`stock-analysis:${normalizedSymbol}`],
})();
}
export async function getAIAnalysis(
symbol: string,
companyName?: string | null,
): Promise<StockAIAnalysis | null> {
const normalizedSymbol = symbol.trim().toUpperCase();
if (!normalizedSymbol || !hasConfiguredAIProvider()) {
return null;
}
return unstable_cache(
async () => {
const [quote, profile, basicFinancials, news] = await Promise.all([
getQuote(normalizedSymbol),
getCompanyProfile(normalizedSymbol),
getBasicFinancials(normalizedSymbol),
getCompanyNews(normalizedSymbol),
]);
const hasAnalysisContext =
Boolean(quote) ||
Boolean(profile) ||
Boolean(basicFinancials?.metric && Object.keys(basicFinancials.metric).length > 0) ||
news.length > 0;
if (!hasAnalysisContext) {
return null;
}
try {
const response = await callAIProviderWithFallback(
buildStockAnalysisPrompt({
symbol: normalizedSymbol,
quote,
profile,
basicFinancials,
sentimentInsights: null,
recentHeadlines: news.slice(0, 4).map(normalizeHeadline),
}),
);
return parseStockAnalysisResponse(
response,
normalizedSymbol,
companyName ?? profile?.name ?? null,
);
} catch (error) {
console.error(`Failed to generate AI stock analysis for ${normalizedSymbol}`, error);
return null;
}
},
['ai-stock-analysis', normalizedSymbol],
{
revalidate: 900,
tags: [`stock-analysis:${normalizedSymbol}`],
},
)();
}