164 lines
5.0 KiB
TypeScript
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}`],
|
|
},
|
|
)();
|
|
}
|