feat: melhora logs e tratamento de erros na análise de redações

- Adiciona logs detalhados em cada etapa do processo
- Melhora validação das variáveis de ambiente
- Implementa tratamento de erros mais robusto
- Padroniza formato de respostas de erro
- Refina schema de validação da OpenAI
This commit is contained in:
Lucas Santana 2025-02-06 22:13:14 -03:00
parent 4609217fb7
commit 0eafbd5350
2 changed files with 205 additions and 103 deletions

View File

@ -1,4 +1,21 @@
export const corsHeaders = { const ALLOWED_ORIGINS = [
'Access-Control-Allow-Origin': '*', 'http://localhost:5173', // Vite dev server
'http://localhost:9999', // Supabase Edge Functions local
'http://localhost', // Vite dev server
'http://localhost:3000', // Caso use outro port
'https://leiturama.ai', // Produção
'https://leiturama.netlify.app' // Staging
];
export function getCorsHeaders(req: Request) {
const origin = req.headers.get('origin') || '';
return {
'Cross-Origin-Resource-Policy': 'cross-origin',
'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0],
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Max-Age': '86400', // 24 horas
'Cross-Origin-Embedder-Policy': 'credentialless'
};
} }

View File

@ -1,6 +1,6 @@
import { serve } from 'https://deno.fresh.run/std@0.168.0/http/server.ts' import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts' import { getCorsHeaders } from '../_shared/cors.ts'
import { OpenAI } from "https://deno.land/x/openai@v4.24.0/mod.ts"; import { OpenAI } from "https://deno.land/x/openai@v4.24.0/mod.ts";
interface EssayAnalysisRequest { interface EssayAnalysisRequest {
@ -50,58 +50,106 @@ interface EssayGenre {
} }
serve(async (req) => { serve(async (req) => {
console.log(`[${new Date().toISOString()}] Nova requisição recebida`)
// Handle CORS // Handle CORS
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders }) console.log('Requisição OPTIONS - CORS preflight')
return new Response('ok', { headers: getCorsHeaders(req) })
} }
try { try {
// Criar cliente Supabase // Criar cliente Supabase
const supabaseClient = createClient( console.log('Inicializando cliente Supabase...')
Deno.env.get('SUPABASE_URL') ?? '', const supabaseUrl = Deno.env.get('SUPABASE_URL')
Deno.env.get('SUPABASE_ANON_KEY') ?? '' const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY')
)
if (!supabaseUrl || !supabaseKey) {
throw new Error('Variáveis de ambiente do Supabase não configuradas')
}
const supabaseClient = createClient(supabaseUrl, supabaseKey)
// Criar cliente OpenAI // 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({ const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY'), apiKey: openaiKey,
}); });
// Obter dados da requisição // Obter dados da requisição
const { essay_id, content, type_id, genre_id }: EssayAnalysisRequest = await req.json() 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 // Validar dados obrigatórios
if (!essay_id || !content || !type_id || !genre_id) { 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( return new Response(
JSON.stringify({ error: 'Dados obrigatórios não fornecidos' }), JSON.stringify({
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } 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 // 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 const { data: typeData, error: typeError } = await supabaseClient
.from('essay_types') .from('essay_types')
.select('*') .select('*')
.eq('id', type_id) .eq('id', type_id)
.single() .single()
if (typeError) {
console.error('Erro ao buscar tipo:', typeError)
}
const { data: genreData, error: genreError } = await supabaseClient const { data: genreData, error: genreError } = await supabaseClient
.from('essay_genres') .from('essay_genres')
.select('*') .select('*')
.eq('id', genre_id) .eq('id', genre_id)
.single() .single()
if (genreError) {
console.error('Erro ao buscar gênero:', genreError)
}
if (typeError || genreError || !typeData || !genreData) { if (typeError || genreError || !typeData || !genreData) {
return new Response( return new Response(
JSON.stringify({ error: 'Tipo ou gênero não encontrado' }), JSON.stringify({
{ status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } 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 essayType: EssayType = typeData
const essayGenre: EssayGenre = genreData const essayGenre: EssayGenre = genreData
console.log('Tipo e gênero encontrados:', {
type: essayType.title,
genre: essayGenre.title
})
// Construir prompt para a análise // 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}". 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: Requisitos específicos do gênero:
@ -143,8 +191,10 @@ Responda em formato JSON seguindo exatamente esta estrutura:
}` }`
// Realizar análise com OpenAI // Realizar análise com OpenAI
console.log('Enviando requisição para OpenAI...')
try {
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-4-turbo-preview", model: "gpt-4o-mini",
messages: [ messages: [
{ {
role: "system", role: "system",
@ -157,19 +207,18 @@ Responda em formato JSON seguindo exatamente esta estrutura:
], ],
response_format: { response_format: {
type: "json_schema", type: "json_schema",
json_schema: {
type: "object",
name: "EssayAnalysis",
schema: { schema: {
type: "object", type: "object",
schema: { required: ["overall_score", "feedback", "strengths", "improvements", "suggestions", "criteria_scores"],
type: "object",
additionalProperties: false,
properties: { properties: {
overall_score: { type: "number" }, overall_score: {
type: "number",
minimum: 0,
maximum: 100
},
feedback: { feedback: {
type: "object", type: "object",
additionalProperties: false, required: ["structure", "content", "language"],
properties: { properties: {
structure: { type: "string" }, structure: { type: "string" },
content: { type: "string" }, content: { type: "string" },
@ -187,14 +236,22 @@ Responda em formato JSON seguindo exatamente esta estrutura:
suggestions: { type: "string" }, suggestions: { type: "string" },
criteria_scores: { criteria_scores: {
type: "object", type: "object",
additionalProperties: false, required: ["adequacy", "coherence", "cohesion", "vocabulary", "grammar"],
properties: { properties: {
adequacy: { type: "number" }, adequacy: {
coherence: { type: "number" }, type: "number"
cohesion: { type: "number" }, },
vocabulary: { type: "number" }, coherence: {
grammar: { type: "number" } type: "number"
} },
cohesion: {
type: "number"
},
vocabulary: {
type: "number"
},
grammar: {
type: "number"
} }
} }
} }
@ -203,9 +260,12 @@ Responda em formato JSON seguindo exatamente esta estrutura:
} }
}); });
console.log('Resposta recebida da OpenAI')
const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content) 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 // Salvar análise no banco
console.log('Salvando análise no banco...')
const { error: saveError } = await supabaseClient const { error: saveError } = await supabaseClient
.from('essay_analyses') .from('essay_analyses')
.insert({ .insert({
@ -219,35 +279,60 @@ Responda em formato JSON seguindo exatamente esta estrutura:
}) })
if (saveError) { if (saveError) {
console.error('Erro ao salvar análise:', saveError)
return new Response( return new Response(
JSON.stringify({ error: 'Erro ao salvar análise' }), JSON.stringify({
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } error: 'Erro ao salvar análise',
details: saveError.message
}),
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
) )
} }
// Atualizar status da redação // Atualizar status da redação
console.log('Atualizando status da redação...')
const { error: updateError } = await supabaseClient const { error: updateError } = await supabaseClient
.from('student_essays') .from('student_essays')
.update({ status: 'analyzed' }) .update({ status: 'analyzed' })
.eq('id', essay_id) .eq('id', essay_id)
if (updateError) { if (updateError) {
console.error('Erro ao atualizar status:', updateError)
return new Response( return new Response(
JSON.stringify({ error: 'Erro ao atualizar status da redação' }), JSON.stringify({
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } 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 // Retornar análise
return new Response( return new Response(
JSON.stringify(analysis), JSON.stringify(analysis),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } } { headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
) )
} catch (error) { } catch (openaiError) {
console.error('Erro na chamada da OpenAI:', openaiError)
return new Response( return new Response(
JSON.stringify({ error: error.message }), JSON.stringify({
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } 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' } }
) )
} }
}) })