Files
ALwrity/frontend/src/api/intentResearchApi.ts

498 lines
17 KiB
TypeScript

/**
* Intent-Driven Research API Client
*
* Client for the new intent-driven research endpoints:
* - /api/research/intent/analyze - Analyze user intent
* - /api/research/intent/research - Execute intent-driven research
*/
import { apiClient, aiApiClient } from './client';
import {
AnalyzeIntentRequest,
AnalyzeIntentResponse,
IntentDrivenResearchRequest,
IntentDrivenResearchResponse,
ResearchIntent,
} from '../components/Research/types/intent.types';
import { WizardState } from '../components/Research/types/research.types';
import { BlogResearchResponse } from '../services/blogWriterApi';
/**
* Analyze user input to understand research intent.
*
* Uses AI to infer:
* - What questions need answering
* - What deliverables user expects (statistics, quotes, case studies)
* - What depth and focus is appropriate
*/
export const analyzeIntent = async (
request: AnalyzeIntentRequest
): Promise<AnalyzeIntentResponse> => {
try {
const { data } = await apiClient.post<AnalyzeIntentResponse>(
'/api/research/intent/analyze',
request
);
return data;
} catch (error: any) {
console.error('[intentResearchApi] analyzeIntent failed:', error);
return {
success: false,
intent: {
primary_question: request.user_input,
secondary_questions: [],
purpose: 'learn',
content_output: 'general',
expected_deliverables: ['key_statistics'],
depth: 'detailed',
focus_areas: [],
also_answering: [],
perspective: null,
time_sensitivity: null,
input_type: 'keywords',
original_input: request.user_input,
confidence: 0.5,
needs_clarification: true,
clarifying_questions: [],
},
analysis_summary: 'Failed to analyze intent',
suggested_queries: [],
suggested_keywords: [],
suggested_angles: [],
quick_options: [],
error_message: error.message || 'Failed to analyze intent',
};
}
};
/**
* Execute research based on user intent.
*
* This is the main endpoint for intent-driven research. It:
* 1. Uses the confirmed intent (or infers from user_input)
* 2. Generates targeted queries for each expected deliverable
* 3. Executes research using Exa/Tavily/Google
* 4. Analyzes results through the lens of user intent
* 5. Returns exactly what the user needs
*/
export const executeIntentResearch = async (
request: IntentDrivenResearchRequest
): Promise<IntentDrivenResearchResponse> => {
try {
const { data } = await apiClient.post<IntentDrivenResearchResponse>(
'/api/research/intent/research',
request
);
return data;
} catch (error: any) {
console.error('[intentResearchApi] executeIntentResearch failed:', error);
return {
success: false,
primary_answer: '',
secondary_answers: {},
statistics: [],
expert_quotes: [],
case_studies: [],
trends: [],
comparisons: [],
best_practices: [],
step_by_step: [],
pros_cons: null,
definitions: {},
examples: [],
predictions: [],
executive_summary: '',
key_takeaways: [],
suggested_outline: [],
sources: [],
confidence: 0,
gaps_identified: [],
follow_up_queries: [],
intent: null,
error_message: error.message || 'Research failed',
};
}
};
/**
* Combined function to analyze intent and execute research in one call.
*
* For simple use cases where user doesn't need to confirm intent.
*/
export const quickIntentResearch = async (
userInput: string,
options?: {
usePersona?: boolean;
useCompetitorData?: boolean;
maxSources?: number;
includeDomains?: string[];
excludeDomains?: string[];
}
): Promise<IntentDrivenResearchResponse> => {
try {
// First analyze intent
const analyzeResponse = await analyzeIntent({
user_input: userInput,
keywords: userInput.split(' ').filter(k => k.length > 2),
use_persona: options?.usePersona ?? true,
use_competitor_data: options?.useCompetitorData ?? true,
});
if (!analyzeResponse.success) {
return {
success: false,
primary_answer: '',
secondary_answers: {},
statistics: [],
expert_quotes: [],
case_studies: [],
trends: [],
comparisons: [],
best_practices: [],
step_by_step: [],
pros_cons: null,
definitions: {},
examples: [],
predictions: [],
executive_summary: '',
key_takeaways: [],
suggested_outline: [],
sources: [],
confidence: 0,
gaps_identified: [],
follow_up_queries: [],
intent: null,
error_message: analyzeResponse.error_message || 'Failed to analyze intent',
};
}
// Execute research with inferred intent
return await executeIntentResearch({
user_input: userInput,
confirmed_intent: analyzeResponse.intent,
selected_queries: analyzeResponse.suggested_queries.slice(0, 5), // Top 5 queries
max_sources: options?.maxSources ?? 10,
include_domains: options?.includeDomains ?? [],
exclude_domains: options?.excludeDomains ?? [],
skip_inference: true, // We already have intent
});
} catch (error: any) {
console.error('[intentResearchApi] quickIntentResearch failed:', error);
return {
success: false,
primary_answer: '',
secondary_answers: {},
statistics: [],
expert_quotes: [],
case_studies: [],
trends: [],
comparisons: [],
best_practices: [],
step_by_step: [],
pros_cons: null,
definitions: {},
examples: [],
predictions: [],
executive_summary: '',
key_takeaways: [],
suggested_outline: [],
sources: [],
confidence: 0,
gaps_identified: [],
follow_up_queries: [],
intent: null,
error_message: error.message || 'Research failed',
};
}
};
/**
* Save research project to Asset Library.
*
* Saves the complete research project state so users can resume later.
*/
export const saveResearchProject = async (
state: WizardState,
options?: {
intentAnalysis?: AnalyzeIntentResponse | null;
confirmedIntent?: ResearchIntent | null;
intentResult?: IntentDrivenResearchResponse | null;
legacyResult?: BlogResearchResponse | null;
title?: string;
description?: string;
projectId?: string; // Project ID for updates (optional)
}
): Promise<{ success: boolean; asset_id?: number; project_id?: string; message: string }> => {
try {
// Generate project title from keywords if not provided
const projectTitle = options?.title ||
(state.keywords.length > 0
? `Research: ${state.keywords.slice(0, 3).join(', ')}`
: 'Research Project');
// Generate description if not provided
const projectDescription = options?.description ||
`Research project on ${state.keywords.join(', ')}. ` +
`Industry: ${state.industry}, Target Audience: ${state.targetAudience}`;
const request = {
project_id: options?.projectId || undefined, // Include project_id for updates
title: projectTitle,
keywords: state.keywords,
industry: state.industry,
target_audience: state.targetAudience,
research_mode: state.researchMode,
config: state.config,
intent_analysis: options?.intentAnalysis ? {
success: options.intentAnalysis.success,
intent: options.intentAnalysis.intent,
analysis_summary: options.intentAnalysis.analysis_summary,
suggested_queries: options.intentAnalysis.suggested_queries,
suggested_keywords: options.intentAnalysis.suggested_keywords,
suggested_angles: options.intentAnalysis.suggested_angles,
quick_options: options.intentAnalysis.quick_options,
trends_config: options.intentAnalysis.trends_config,
} : null,
confirmed_intent: options?.confirmedIntent || null,
intent_result: options?.intentResult ? {
success: options.intentResult.success,
primary_answer: options.intentResult.primary_answer,
secondary_answers: options.intentResult.secondary_answers,
statistics: options.intentResult.statistics,
expert_quotes: options.intentResult.expert_quotes,
case_studies: options.intentResult.case_studies,
trends: options.intentResult.trends,
comparisons: options.intentResult.comparisons,
best_practices: options.intentResult.best_practices,
step_by_step: options.intentResult.step_by_step,
pros_cons: options.intentResult.pros_cons,
definitions: options.intentResult.definitions,
examples: options.intentResult.examples,
predictions: options.intentResult.predictions,
executive_summary: options.intentResult.executive_summary,
key_takeaways: options.intentResult.key_takeaways,
suggested_outline: options.intentResult.suggested_outline,
sources: options.intentResult.sources,
confidence: options.intentResult.confidence,
gaps_identified: options.intentResult.gaps_identified,
follow_up_queries: options.intentResult.follow_up_queries,
intent: options.intentResult.intent,
google_trends_data: options.intentResult.google_trends_data,
} : null,
legacy_result: options?.legacyResult || null,
current_step: state.currentStep,
description: projectDescription,
};
const { data } = await apiClient.post<{ success: boolean; asset_id?: number; project_id?: string; message: string }>(
'/api/research/projects/save',
request
);
// After saving project, also save to ContentAsset library (following podcast maker pattern)
if (data.success && data.project_id) {
try {
await saveResearchProjectToAssetLibrary({
projectId: data.project_id,
title: projectTitle,
description: projectDescription,
keywords: state.keywords,
industry: state.industry,
targetAudience: state.targetAudience,
researchMode: state.researchMode,
config: state.config,
status: (options?.intentResult || options?.legacyResult) ? 'completed' : (options?.intentAnalysis ? 'draft' : 'draft'),
currentStep: state.currentStep,
});
console.log(`[intentResearchApi] ✅ Research project saved to asset library: project_id=${data.project_id}, status=${(options?.intentResult || options?.legacyResult) ? 'completed' : 'draft'}`);
} catch (error) {
console.warn('[intentResearchApi] Failed to save research project to asset library:', error);
// Don't fail the whole operation if asset creation fails
}
}
return data;
} catch (error: any) {
console.error('[intentResearchApi] saveResearchProject failed:', error);
return {
success: false,
message: error.message || 'Failed to save research project',
};
}
};
/**
* Save research project to Asset Library (ContentAsset).
* Following podcast maker pattern: podcastApi.saveAudioToAssetLibrary()
*/
/**
* Save research project to Asset Library (ContentAsset).
* Following podcast maker pattern: podcastApi.saveAudioToAssetLibrary()
*
* Checks for existing asset with same project_id and updates it, otherwise creates new one.
*/
export const saveResearchProjectToAssetLibrary = async (params: {
projectId: string;
title: string;
description?: string;
keywords: string[];
industry?: string;
targetAudience?: string;
researchMode?: string;
config?: any;
status?: string;
currentStep?: number;
}): Promise<{ assetId: number }> => {
try {
const fileUrl = `/api/research/projects/${params.projectId}/export`;
const assetMetadata = {
project_type: 'research_project',
project_id: params.projectId,
status: params.status || 'draft',
keywords: params.keywords,
industry: params.industry,
target_audience: params.targetAudience,
research_mode: params.researchMode,
current_step: params.currentStep || 1,
};
// Check if asset already exists for this project_id
try {
const searchResponse = await aiApiClient.get('/api/content-assets/', {
params: {
asset_type: 'text',
source_module: 'research_tools',
search: params.projectId,
limit: 100,
},
});
// Find existing asset with matching project_id in metadata
const existingAsset = searchResponse.data.assets?.find(
(asset: any) =>
asset.asset_metadata?.project_type === 'research_project' &&
asset.asset_metadata?.project_id === params.projectId
);
if (existingAsset) {
// Update existing asset
const updateResponse = await aiApiClient.put(`/api/content-assets/${existingAsset.id}`, {
title: params.title,
description: params.description || `Research project on ${params.keywords.slice(0, 3).join(', ')}`,
tags: ['research', 'research_project', params.projectId, ...params.keywords.slice(0, 5)],
asset_metadata: assetMetadata,
});
console.log(`[intentResearchApi] Updated existing ContentAsset for project ${params.projectId}: asset_id=${existingAsset.id}`);
return { assetId: updateResponse.data.id };
}
} catch (searchError) {
console.warn('[intentResearchApi] Failed to search for existing asset, creating new one:', searchError);
// Continue to create new asset if search fails
}
// Create new asset if none exists
const response = await aiApiClient.post('/api/content-assets/', {
asset_type: 'text',
source_module: 'research_tools',
filename: `research_${params.projectId}.json`,
file_url: fileUrl,
title: params.title,
description: params.description || `Research project on ${params.keywords.slice(0, 3).join(', ')}`,
tags: ['research', 'research_project', params.projectId, ...params.keywords.slice(0, 5)],
asset_metadata: assetMetadata,
cost: 0,
mime_type: 'application/json',
});
console.log(`[intentResearchApi] ✅ Created new ContentAsset for project ${params.projectId}: asset_id=${response.data.id}, status=${params.status || 'draft'}, source_module=research_tools, asset_type=text`);
return { assetId: response.data.id };
} catch (error: any) {
console.error('[intentResearchApi] saveResearchProjectToAssetLibrary failed:', error);
throw error;
}
};
/**
* List research projects.
*/
export const listResearchProjects = async (params?: {
status?: string;
is_favorite?: boolean;
limit?: number;
offset?: number;
}): Promise<{ projects: any[]; total: number; limit: number; offset: number }> => {
try {
const { data } = await apiClient.get<{ projects: any[]; total: number; limit: number; offset: number }>(
'/api/research/projects',
{ params }
);
return data;
} catch (error: any) {
console.error('[intentResearchApi] listResearchProjects failed:', error);
return {
projects: [],
total: 0,
limit: params?.limit || 50,
offset: params?.offset || 0,
};
}
};
/**
* Get a single research project by ID.
*/
export const getResearchProject = async (projectId: string): Promise<any> => {
try {
const { data } = await apiClient.get(`/api/research/projects/${projectId}`);
return data;
} catch (error: any) {
console.error('[intentResearchApi] getResearchProject failed:', error);
throw error;
}
};
/**
* Delete a research project.
*/
export const deleteResearchProject = async (projectId: string): Promise<void> => {
try {
await apiClient.delete(`/api/research/projects/${projectId}`);
} catch (error: any) {
console.error('[intentResearchApi] deleteResearchProject failed:', error);
throw error;
}
};
/**
* Toggle favorite status of a research project.
*/
export const toggleResearchProjectFavorite = async (projectId: string): Promise<any> => {
try {
// First get the project to check current favorite status
const project = await getResearchProject(projectId);
const newFavoriteStatus = !project.is_favorite;
// Update the project with new favorite status
const { data } = await apiClient.put(`/api/research/projects/${projectId}`, {
is_favorite: newFavoriteStatus,
});
return data;
} catch (error: any) {
console.error('[intentResearchApi] toggleResearchProjectFavorite failed:', error);
throw error;
}
};
export const intentResearchApi = {
analyzeIntent,
executeIntentResearch,
quickIntentResearch,
saveResearchProject,
saveResearchProjectToAssetLibrary,
listResearchProjects,
getResearchProject,
deleteResearchProject,
toggleResearchProjectFavorite,
};
export default intentResearchApi;