story-generator/supabase/functions/analyze-essay/index.ts

467 lines
15 KiB
TypeScript

import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { getCorsHeaders } from '../_shared/cors.ts'
import { OpenAI } from "https://deno.land/x/openai@v4.24.0/mod.ts";
interface EssayAnalysisRequest {
essay_id: string;
content: string;
type_id: string;
genre_id: string;
}
interface EssayAnalysisResponse {
overall_score: number;
suggestions: string;
feedback: {
structure: string;
content: string;
language: string;
};
strengths: string[];
improvements: string[];
criteria_scores: {
adequacy: number;
coherence: number;
cohesion: number;
vocabulary: number;
grammar: number;
};
}
interface EssayType {
id: string;
slug: string;
title: string;
description: string;
}
interface EssayGenre {
id: string;
type_id: string;
slug: string;
title: string;
description: string;
requirements: {
min_words: number;
max_words: number;
required_elements: string[];
};
}
serve(async (req) => {
console.log(`[${new Date().toISOString()}] Nova requisição recebida`)
// Handle CORS
if (req.method === 'OPTIONS') {
console.log('Requisição OPTIONS - CORS preflight')
return new Response('ok', { headers: getCorsHeaders(req) })
}
try {
// Criar cliente Supabase
console.log('Inicializando cliente Supabase...')
const supabaseUrl = Deno.env.get('SUPABASE_URL')
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error('Variáveis de ambiente do Supabase não configuradas')
}
const supabaseClient = createClient(supabaseUrl, supabaseServiceKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
}
})
// Criar cliente OpenAI
console.log('Inicializando cliente OpenAI...')
const openaiKey = Deno.env.get('OPENAI_API_KEY')
if (!openaiKey) {
throw new Error('OPENAI_API_KEY não configurada')
}
const openai = new OpenAI({
apiKey: openaiKey,
});
// Obter dados da requisição
console.log('Obtendo dados da requisição...')
const requestData = await req.json()
console.log('Dados recebidos:', JSON.stringify(requestData, null, 2))
const { essay_id, content, type_id, genre_id }: EssayAnalysisRequest = requestData
// Validar dados obrigatórios
if (!essay_id || !content || !type_id || !genre_id) {
console.error('Dados obrigatórios faltando:', { essay_id, content: !!content, type_id, genre_id })
return new Response(
JSON.stringify({
error: 'Dados obrigatórios não fornecidos',
details: {
essay_id: !essay_id,
content: !content,
type_id: !type_id,
genre_id: !genre_id
}
}),
{ status: 400, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
// Buscar informações do tipo e gênero
console.log('Buscando informações do tipo e gênero...')
const { data: typeData, error: typeError } = await supabaseClient
.from('essay_types')
.select('*')
.eq('id', type_id)
.single()
if (typeError) {
console.error('Erro ao buscar tipo:', typeError)
}
const { data: genreData, error: genreError } = await supabaseClient
.from('essay_genres')
.select('*')
.eq('id', genre_id)
.single()
if (genreError) {
console.error('Erro ao buscar gênero:', genreError)
}
if (typeError || genreError || !typeData || !genreData) {
return new Response(
JSON.stringify({
error: 'Tipo ou gênero não encontrado',
details: {
typeError: typeError?.message,
genreError: genreError?.message
}
}),
{ status: 404, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
const essayType: EssayType = typeData
const essayGenre: EssayGenre = genreData
console.log('Tipo e gênero encontrados:', {
type: essayType.title,
genre: essayGenre.title
})
// Construir prompt para a análise
console.log('Construindo prompt para análise...')
const prompt = `Você é um professor especialista em análise de textos. Analise a redação a seguir considerando que é do tipo "${essayType.title}" e gênero "${essayGenre.title}".
Requisitos específicos do gênero:
- Mínimo de palavras: ${essayGenre.requirements.min_words}
- Máximo de palavras: ${essayGenre.requirements.max_words}
- Elementos obrigatórios: ${essayGenre.requirements.required_elements.join(', ')}
Texto para análise:
${content}
Forneça uma análise detalhada considerando:
1. Adequação ao tipo e gênero textual
2. Coerência e coesão textual
3. Vocabulário e linguagem
4. Gramática e ortografia
5. Elementos obrigatórios do gênero
6. Pontos fortes
7. Pontos a melhorar
8. Sugestões específicas para aprimoramento
Responda em formato JSON seguindo exatamente esta estrutura:
{
"overall_score": number, // 0 a 100
"suggestions": string, // Sugestões específicas
"feedback": {
"structure": string, // Feedback sobre estrutura e organização
"content": string, // Feedback sobre conteúdo e ideias
"language": string // Feedback sobre linguagem e gramática
},
"strengths": string[], // Lista de pontos fortes
"improvements": string[], // Lista de pontos a melhorar
"criteria_scores": {
"adequacy": number, // 0 a 100 - Adequação ao tema/gênero
"coherence": number, // 0 a 100 - Coerência textual
"cohesion": number, // 0 a 100 - Coesão
"vocabulary": number, // 0 a 100 - Vocabulário
"grammar": number // 0 a 100 - Gramática e ortografia
}
}`
// Realizar análise com OpenAI
console.log('Enviando requisição para OpenAI...')
try {
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "Você é um professor especialista em análise de textos, com vasta experiência em avaliação de redações."
},
{
role: "user",
content: prompt
}
],
response_format: {
type: "json_schema",
json_schema: {
type: "object",
required: ["overall_score", "suggestions", "feedback", "strengths", "improvements", "criteria_scores"],
properties: {
overall_score: {
type: "number",
minimum: 0,
maximum: 100,
description: "Pontuação geral da redação (0-100)"
},
suggestions: {
type: "string",
description: "Sugestões específicas para aprimoramento"
},
feedback: {
type: "object",
required: ["structure", "content", "language"],
properties: {
structure: {
type: "string",
description: "Feedback sobre estrutura e organização"
},
content: {
type: "string",
description: "Feedback sobre conteúdo e ideias"
},
language: {
type: "string",
description: "Feedback sobre linguagem e gramática"
}
}
},
strengths: {
type: "array",
items: {
type: "string",
description: "Ponto forte da redação"
},
minItems: 1,
description: "Lista de pontos fortes da redação"
},
improvements: {
type: "array",
items: {
type: "string",
description: "Ponto a melhorar na redação"
},
minItems: 1,
description: "Lista de pontos a melhorar na redação"
},
criteria_scores: {
type: "object",
required: ["adequacy", "coherence", "cohesion", "vocabulary", "grammar"],
properties: {
adequacy: {
type: "number",
minimum: 0,
maximum: 100,
description: "Adequação ao tema/gênero (0-100)"
},
coherence: {
type: "number",
minimum: 0,
maximum: 100,
description: "Coerência textual (0-100)"
},
cohesion: {
type: "number",
minimum: 0,
maximum: 100,
description: "Coesão textual (0-100)"
},
vocabulary: {
type: "number",
minimum: 0,
maximum: 100,
description: "Vocabulário (0-100)"
},
grammar: {
type: "number",
minimum: 0,
maximum: 100,
description: "Gramática e ortografia (0-100)"
}
}
}
}
}
}
});
console.log('Resposta recebida da OpenAI')
const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content)
console.log('Análise gerada:', JSON.stringify(analysis, null, 2))
// Salvar análise no banco
console.log('Salvando análise no banco...')
// Primeiro, criar a análise principal
const { data: analysisData, error: analysisError } = await supabaseClient
.from('essay_analyses')
.insert({
essay_id,
overall_score: analysis.overall_score,
suggestions: analysis.suggestions
})
.select()
.single()
if (analysisError) {
console.error('Erro ao salvar análise principal:', analysisError)
return new Response(
JSON.stringify({
error: 'Erro ao salvar análise',
details: analysisError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
// Salvar feedback
const { error: feedbackError } = await supabaseClient
.from('essay_analysis_feedback')
.insert({
analysis_id: analysisData.id,
structure_feedback: analysis.feedback.structure,
content_feedback: analysis.feedback.content,
language_feedback: analysis.feedback.language
})
if (feedbackError) {
console.error('Erro ao salvar feedback:', feedbackError)
return new Response(
JSON.stringify({
error: 'Erro ao salvar feedback',
details: feedbackError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
// Salvar pontos fortes
const strengths = analysis.strengths.map(strength => ({
analysis_id: analysisData.id,
strength
}))
const { error: strengthsError } = await supabaseClient
.from('essay_analysis_strengths')
.insert(strengths)
if (strengthsError) {
console.error('Erro ao salvar pontos fortes:', strengthsError)
return new Response(
JSON.stringify({
error: 'Erro ao salvar pontos fortes',
details: strengthsError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
// Salvar pontos a melhorar
const improvements = analysis.improvements.map(improvement => ({
analysis_id: analysisData.id,
improvement
}))
const { error: improvementsError } = await supabaseClient
.from('essay_analysis_improvements')
.insert(improvements)
if (improvementsError) {
console.error('Erro ao salvar pontos a melhorar:', improvementsError)
return new Response(
JSON.stringify({
error: 'Erro ao salvar pontos a melhorar',
details: improvementsError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
// Salvar notas por critério
const { error: scoresError } = await supabaseClient
.from('essay_analysis_scores')
.insert({
analysis_id: analysisData.id,
adequacy: analysis.criteria_scores.adequacy,
coherence: analysis.criteria_scores.coherence,
cohesion: analysis.criteria_scores.cohesion,
vocabulary: analysis.criteria_scores.vocabulary,
grammar: analysis.criteria_scores.grammar
})
if (scoresError) {
console.error('Erro ao salvar notas:', scoresError)
return new Response(
JSON.stringify({
error: 'Erro ao salvar notas',
details: scoresError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
// Atualizar status da redação
console.log('Atualizando status da redação...')
const { error: updateError } = await supabaseClient
.from('student_essays')
.update({ status: 'analyzed' })
.eq('id', essay_id)
if (updateError) {
console.error('Erro ao atualizar status:', updateError)
return new Response(
JSON.stringify({
error: 'Erro ao atualizar status da redação',
details: updateError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
console.log('Análise concluída com sucesso')
// Retornar análise
return new Response(
JSON.stringify(analysis),
{ headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
} catch (openaiError) {
console.error('Erro na chamada da OpenAI:', openaiError)
return new Response(
JSON.stringify({
error: 'Erro ao gerar análise',
details: openaiError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
} catch (error) {
console.error('Erro geral na função:', error)
return new Response(
JSON.stringify({
error: 'Erro interno do servidor',
details: error.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
)
}
})