mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
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:
parent
4609217fb7
commit
0eafbd5350
@ -1,4 +1,21 @@
|
|||||||
export const corsHeaders = {
|
const ALLOWED_ORIGINS = [
|
||||||
'Access-Control-Allow-Origin': '*',
|
'http://localhost:5173', // Vite dev server
|
||||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
'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-Max-Age': '86400', // 24 horas
|
||||||
|
'Cross-Origin-Embedder-Policy': 'credentialless'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@ -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,111 +191,148 @@ Responda em formato JSON seguindo exatamente esta estrutura:
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
// Realizar análise com OpenAI
|
// Realizar análise com OpenAI
|
||||||
const completion = await openai.chat.completions.create({
|
console.log('Enviando requisição para OpenAI...')
|
||||||
model: "gpt-4-turbo-preview",
|
try {
|
||||||
messages: [
|
const completion = await openai.chat.completions.create({
|
||||||
{
|
model: "gpt-4o-mini",
|
||||||
role: "system",
|
messages: [
|
||||||
content: "Você é um professor especialista em análise de textos, com vasta experiência em avaliação de redações."
|
{
|
||||||
},
|
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
|
{
|
||||||
}
|
role: "user",
|
||||||
],
|
content: prompt
|
||||||
response_format: {
|
}
|
||||||
type: "json_schema",
|
],
|
||||||
json_schema: {
|
response_format: {
|
||||||
type: "object",
|
type: "json_schema",
|
||||||
name: "EssayAnalysis",
|
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
schema: {
|
required: ["overall_score", "feedback", "strengths", "improvements", "suggestions", "criteria_scores"],
|
||||||
type: "object",
|
properties: {
|
||||||
additionalProperties: false,
|
overall_score: {
|
||||||
properties: {
|
type: "number",
|
||||||
overall_score: { type: "number" },
|
minimum: 0,
|
||||||
feedback: {
|
maximum: 100
|
||||||
type: "object",
|
},
|
||||||
additionalProperties: false,
|
feedback: {
|
||||||
properties: {
|
type: "object",
|
||||||
structure: { type: "string" },
|
required: ["structure", "content", "language"],
|
||||||
content: { type: "string" },
|
properties: {
|
||||||
language: { type: "string" }
|
structure: { type: "string" },
|
||||||
}
|
content: { type: "string" },
|
||||||
},
|
language: { type: "string" }
|
||||||
strengths: {
|
}
|
||||||
type: "array",
|
},
|
||||||
items: { type: "string" }
|
strengths: {
|
||||||
},
|
type: "array",
|
||||||
improvements: {
|
items: { type: "string" }
|
||||||
type: "array",
|
},
|
||||||
items: { type: "string" }
|
improvements: {
|
||||||
},
|
type: "array",
|
||||||
suggestions: { type: "string" },
|
items: { type: "string" }
|
||||||
criteria_scores: {
|
},
|
||||||
type: "object",
|
suggestions: { type: "string" },
|
||||||
additionalProperties: false,
|
criteria_scores: {
|
||||||
properties: {
|
type: "object",
|
||||||
adequacy: { type: "number" },
|
required: ["adequacy", "coherence", "cohesion", "vocabulary", "grammar"],
|
||||||
coherence: { type: "number" },
|
properties: {
|
||||||
cohesion: { type: "number" },
|
adequacy: {
|
||||||
vocabulary: { type: "number" },
|
type: "number"
|
||||||
grammar: { type: "number" }
|
},
|
||||||
|
coherence: {
|
||||||
|
type: "number"
|
||||||
|
},
|
||||||
|
cohesion: {
|
||||||
|
type: "number"
|
||||||
|
},
|
||||||
|
vocabulary: {
|
||||||
|
type: "number"
|
||||||
|
},
|
||||||
|
grammar: {
|
||||||
|
type: "number"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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...')
|
||||||
|
const { error: saveError } = await supabaseClient
|
||||||
|
.from('essay_analyses')
|
||||||
|
.insert({
|
||||||
|
essay_id,
|
||||||
|
overall_score: analysis.overall_score,
|
||||||
|
feedback: analysis.feedback,
|
||||||
|
strengths: analysis.strengths,
|
||||||
|
improvements: analysis.improvements,
|
||||||
|
suggestions: analysis.suggestions,
|
||||||
|
criteria_scores: analysis.criteria_scores
|
||||||
|
})
|
||||||
|
|
||||||
|
if (saveError) {
|
||||||
|
console.error('Erro ao salvar análise:', saveError)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Erro ao salvar análise',
|
||||||
|
details: saveError.message
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content)
|
// 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)
|
||||||
|
|
||||||
// Salvar análise no banco
|
if (updateError) {
|
||||||
const { error: saveError } = await supabaseClient
|
console.error('Erro ao atualizar status:', updateError)
|
||||||
.from('essay_analyses')
|
return new Response(
|
||||||
.insert({
|
JSON.stringify({
|
||||||
essay_id,
|
error: 'Erro ao atualizar status da redação',
|
||||||
overall_score: analysis.overall_score,
|
details: updateError.message
|
||||||
feedback: analysis.feedback,
|
}),
|
||||||
strengths: analysis.strengths,
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||||
improvements: analysis.improvements,
|
)
|
||||||
suggestions: analysis.suggestions,
|
}
|
||||||
criteria_scores: analysis.criteria_scores
|
|
||||||
})
|
|
||||||
|
|
||||||
if (saveError) {
|
console.log('Análise concluída com sucesso')
|
||||||
|
// Retornar análise
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: 'Erro ao salvar análise' }),
|
JSON.stringify(analysis),
|
||||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ 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' } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualizar status da redação
|
|
||||||
const { error: updateError } = await supabaseClient
|
|
||||||
.from('student_essays')
|
|
||||||
.update({ status: 'analyzed' })
|
|
||||||
.eq('id', essay_id)
|
|
||||||
|
|
||||||
if (updateError) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: 'Erro ao atualizar status da redação' }),
|
|
||||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retornar análise
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify(analysis),
|
|
||||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Erro geral na função:', error)
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({ error: error.message }),
|
JSON.stringify({
|
||||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
error: 'Erro interno do servidor',
|
||||||
|
details: error.message
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Loading…
Reference in New Issue
Block a user