Actualizar database-types.ts #27
@@ -20,9 +20,9 @@ jobs:
|
||||
|
||||
- name: Verify generated types are checked in
|
||||
run: |
|
||||
supabase gen types typescript --local > database-types.ts
|
||||
if ! git diff --ignore-space-at-eol --exit-code --quiet database-types.ts; then
|
||||
echo "Detected uncommitted changes after build. See status below:"
|
||||
supabase gen types typescript --local > supabase/functions/_shared/database.types.ts
|
||||
if ! git diff --ignore-space-at-eol --exit-code --quiet supabase/functions/_shared/database.types.ts; then
|
||||
echo "Detected uncommitted changes after build. See status below:"
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Writing Supabase Edge Functions
|
||||
|
||||
You're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices:
|
||||
|
||||
## Guidelines
|
||||
|
||||
1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)
|
||||
2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.
|
||||
3. Do NOT use bare specifiers when importing dependencies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.
|
||||
4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.
|
||||
5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.
|
||||
6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from "node:process". Use Node APIs when you find gaps in Deno APIs.
|
||||
7. Do NOT use `import { serve } from "https://deno.land/std@0.168.0/http/server.ts"`. Instead use the built-in `Deno.serve`.
|
||||
8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:
|
||||
- SUPABASE_URL
|
||||
- SUPABASE_ANON_KEY
|
||||
- SUPABASE_SERVICE_ROLE_KEY
|
||||
- SUPABASE_DB_URL
|
||||
9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file`
|
||||
10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly.
|
||||
11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs.
|
||||
12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.
|
||||
|
||||
## Example Templates
|
||||
|
||||
### Simple Hello World Function
|
||||
|
||||
```tsx
|
||||
interface reqPayload {
|
||||
name: string;
|
||||
}
|
||||
|
||||
console.info("server started");
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const { name }: reqPayload = await req.json();
|
||||
const data = {
|
||||
message: `Hello ${name} from foo!`,
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { "Content-Type": "application/json", Connection: "keep-alive" },
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Example Function using Node built-in API
|
||||
|
||||
```tsx
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import process from "node:process";
|
||||
|
||||
const generateRandomString = (length) => {
|
||||
const buffer = randomBytes(length);
|
||||
return buffer.toString("hex");
|
||||
};
|
||||
|
||||
const randomString = generateRandomString(10);
|
||||
console.log(randomString);
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const message = `Hello`;
|
||||
res.end(message);
|
||||
});
|
||||
|
||||
server.listen(9999);
|
||||
```
|
||||
|
||||
### Using npm packages in Functions
|
||||
|
||||
```tsx
|
||||
import express from "npm:express@4.18.2";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get(/(.*)/, (req, res) => {
|
||||
res.send("Welcome to Supabase");
|
||||
});
|
||||
|
||||
app.listen(8000);
|
||||
```
|
||||
|
||||
### Generate embeddings using built-in @Supabase.ai API
|
||||
|
||||
```tsx
|
||||
const model = new Supabase.ai.Session("gte-small");
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams;
|
||||
const input = params.get("text");
|
||||
const output = await model.run(input, { mean_pool: true, normalize: true });
|
||||
return new Response(JSON.stringify(output), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
"allowScripts": [
|
||||
"npm:deno@2.6.4",
|
||||
"npm:supabase-js@1.0.4",
|
||||
"npm:supabase@2.72.6"
|
||||
"npm:supabase@2.75.0"
|
||||
],
|
||||
"imports": {
|
||||
"@openai/openai": "jsr:@openai/openai@^6.16.0",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"jsr:@david/dax@~0.44.2": "0.44.2",
|
||||
"jsr:@david/path@0.2": "0.2.0",
|
||||
"jsr:@david/which@~0.4.1": "0.4.1",
|
||||
"jsr:@helpr/express@*": "0.1.1",
|
||||
"jsr:@openai/openai@*": "6.16.0",
|
||||
"jsr:@openai/openai@^6.16.0": "6.16.0",
|
||||
"jsr:@std/bytes@^1.0.6": "1.0.6",
|
||||
@@ -20,12 +21,15 @@
|
||||
"jsr:@zod/zod@^4.3.5": "4.3.5",
|
||||
"npm:@supabase/supabase-js@2": "2.90.1",
|
||||
"npm:@supabase/supabase-js@^2.90.1": "2.90.1",
|
||||
"npm:@toon-format/toon@^2.1.0": "2.1.0",
|
||||
"npm:@types/bun@^1.3.5": "1.3.5",
|
||||
"npm:citeproc@^2.4.63": "2.4.63",
|
||||
"npm:deno@^2.6.4": "2.6.4",
|
||||
"npm:jwt-decode@4": "4.0.0",
|
||||
"npm:openai@6.16.0": "6.16.0_zod@3.25.76",
|
||||
"npm:openai@^4.52.5": "4.104.0_zod@3.25.76",
|
||||
"npm:openai@^6.16.0": "6.16.0_zod@3.25.76",
|
||||
"npm:supabase@^2.72.6": "2.72.6",
|
||||
"npm:supabase@2.75.0": "2.75.0",
|
||||
"npm:zod@3": "3.25.76"
|
||||
},
|
||||
"jsr": {
|
||||
@@ -54,6 +58,9 @@
|
||||
"@david/which@0.4.1": {
|
||||
"integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e"
|
||||
},
|
||||
"@helpr/express@0.1.1": {
|
||||
"integrity": "15391b350b92ab5102919c555179c954ff3d5974d7f170b99880836e249756d2"
|
||||
},
|
||||
"@openai/openai@6.16.0": {
|
||||
"integrity": "ccee548f61c382d715091fff0c2c3390a4487823644430b8235df543d6d6a78b",
|
||||
"dependencies": [
|
||||
@@ -182,6 +189,9 @@
|
||||
"@supabase/storage-js"
|
||||
]
|
||||
},
|
||||
"@toon-format/toon@2.1.0": {
|
||||
"integrity": "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="
|
||||
},
|
||||
"@types/bun@1.3.5": {
|
||||
"integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==",
|
||||
"dependencies": [
|
||||
@@ -254,6 +264,9 @@
|
||||
"chownr@3.0.0": {
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
|
||||
},
|
||||
"citeproc@2.4.63": {
|
||||
"integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q=="
|
||||
},
|
||||
"cmd-shim@8.0.0": {
|
||||
"integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="
|
||||
},
|
||||
@@ -415,6 +428,9 @@
|
||||
"imurmurhash@0.1.4": {
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
|
||||
},
|
||||
"jwt-decode@4.0.0": {
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||
},
|
||||
"math-intrinsics@1.1.0": {
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
||||
},
|
||||
@@ -496,8 +512,8 @@
|
||||
"signal-exit@4.1.0": {
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
||||
},
|
||||
"supabase@2.72.6": {
|
||||
"integrity": "sha512-axO1MkZgwMIxj78vKiGosnuXwzOaQq0Kz0I8jheNMwJH9dKGf3q/arivYT44z8u3t5FjGUxQWOuQoJetzcmm/A==",
|
||||
"supabase@2.75.0": {
|
||||
"integrity": "sha512-Xaai8Wp7F03OMkOWSyS1OMMdk5DxkauJxcMMesj4mO6jDGEZpWBwgo/gIUb4hsckexmr/RXYdpDpTbUIMXahbw==",
|
||||
"dependencies": [
|
||||
"bin-links",
|
||||
"https-proxy-agent",
|
||||
@@ -507,8 +523,8 @@
|
||||
"scripts": true,
|
||||
"bin": true
|
||||
},
|
||||
"tar@7.5.2": {
|
||||
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
||||
"tar@7.5.7": {
|
||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||
"dependencies": [
|
||||
"@isaacs/fs-minipass",
|
||||
"chownr",
|
||||
@@ -561,6 +577,9 @@
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.224.0/dotenv/mod.ts": "0180eaeedaaf88647318811cdaa418cc64dc51fb08354f91f5f480d0a1309f7d",
|
||||
"https://deno.land/std@0.224.0/dotenv/parse.ts": "09977ff88dfd1f24f9973a338f0f91bbdb9307eb5ff6085446e7c423e4c7ba0c",
|
||||
"https://deno.land/std@0.224.0/dotenv/stringify.ts": "275da322c409170160440836342eaa7cf012a1d11a7e700d8ca4e7f2f8aa4615",
|
||||
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
||||
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
||||
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
||||
@@ -587,10 +606,13 @@
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@supabase/supabase-js@^2.90.1",
|
||||
"npm:@toon-format/toon@^2.1.0",
|
||||
"npm:@types/bun@^1.3.5",
|
||||
"npm:citeproc@^2.4.63",
|
||||
"npm:deno@^2.6.4",
|
||||
"npm:jwt-decode@4",
|
||||
"npm:openai@^6.16.0",
|
||||
"npm:supabase@^2.72.6"
|
||||
"npm:supabase@2.75.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2273
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,577 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "bb61ec88",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { load } from \"jsr:@std/dotenv\";\n",
|
||||
"import OpenAI from \"jsr:@openai/openai\";\n",
|
||||
"\n",
|
||||
"const env = await load({\n",
|
||||
" export: true,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"const openai = new OpenAI();\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "8baca5f2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { createClient } from \"@supabase/supabase-js\";\n",
|
||||
"const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n",
|
||||
"const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n",
|
||||
"const supabase = createClient(supabaseUrl, supabaseKey);\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "923b8e08",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Esperando a que la IA termine con la asignatura fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4...\n",
|
||||
"Conexión WebSocket establecida. La celda está esperando cambios...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ename": "",
|
||||
"evalue": "",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n",
|
||||
"\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n",
|
||||
"\u001b[1;31mClick <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. \n",
|
||||
"\u001b[1;31mView Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details."
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"const ID_ASIGNATURA = \"fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4\";\n",
|
||||
"console.log(\n",
|
||||
" `Esperando a que la IA termine con la asignatura ${ID_ASIGNATURA}...`,\n",
|
||||
");\n",
|
||||
"\n",
|
||||
"if (!supabase) {\n",
|
||||
" console.log(\"No hay supabase\");\n",
|
||||
"} else {\n",
|
||||
" // 2. Armas la \"trampa\" (el listener)\n",
|
||||
" await new Promise((resolve) => {\n",
|
||||
" const canal = supabase\n",
|
||||
" .channel(\"test-asignatura\")\n",
|
||||
" .on(\n",
|
||||
" \"postgres_changes\",\n",
|
||||
" {\n",
|
||||
" event: \"UPDATE\",\n",
|
||||
" schema: \"public\",\n",
|
||||
" table: \"asignaturas\",\n",
|
||||
" filter: `id=eq.${ID_ASIGNATURA}`,\n",
|
||||
" },\n",
|
||||
" (payload) => {\n",
|
||||
" const nuevoEstado = payload.new.estado;\n",
|
||||
"\n",
|
||||
" if (nuevoEstado !== \"generando\") {\n",
|
||||
" console.log(\n",
|
||||
" `¡Proceso completado! El nuevo estado es: ${nuevoEstado}`,\n",
|
||||
" );\n",
|
||||
"\n",
|
||||
" supabase.removeChannel(canal);\n",
|
||||
" resolve(true); // ¡Aquí destrabamos la celda de Jupyter!\n",
|
||||
" } else {\n",
|
||||
" console.log(\"Hubo un update, pero sigue generando...\");\n",
|
||||
" }\n",
|
||||
" },\n",
|
||||
" )\n",
|
||||
" .subscribe((status) => {\n",
|
||||
" if (status === \"SUBSCRIBED\") {\n",
|
||||
" console.log(\n",
|
||||
" \"Conexión WebSocket establecida. La celda está esperando cambios...\",\n",
|
||||
" );\n",
|
||||
" }\n",
|
||||
" });\n",
|
||||
" });\n",
|
||||
"\n",
|
||||
" console.log(\"Script finalizado correctamente.\");\n",
|
||||
"}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8a1509d8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Iniciando conexión abierta y sin filtros...\n",
|
||||
"Conectado. Esperando cualquier cambio en la tabla asignaturas...\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ename": "",
|
||||
"evalue": "",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n",
|
||||
"\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n",
|
||||
"\u001b[1;31mClick <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. \n",
|
||||
"\u001b[1;31mView Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details."
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"console.log(`Iniciando conexión abierta y sin filtros...`);\n",
|
||||
"\n",
|
||||
"await new Promise((resolve) => {\n",
|
||||
" const canal = supabase\n",
|
||||
" .channel(\"test-global-asignaturas\")\n",
|
||||
" .on(\n",
|
||||
" \"postgres_changes\",\n",
|
||||
" {\n",
|
||||
" event: \"*\", // Escuchamos absolutamente TODO\n",
|
||||
" schema: \"public\",\n",
|
||||
" table: \"asignaturas\",\n",
|
||||
" },\n",
|
||||
" (payload) => {\n",
|
||||
" // Imprimimos todo el objeto tal como llega del servidor\n",
|
||||
" console.log(\"🚨 ¡EVENTO RECIBIDO EN CRUDO!\", payload);\n",
|
||||
"\n",
|
||||
" // Cerramos la conexión para liberar la celda\n",
|
||||
" supabase.removeChannel(canal);\n",
|
||||
" resolve(true);\n",
|
||||
" },\n",
|
||||
" )\n",
|
||||
" .subscribe((status, err) => {\n",
|
||||
" if (status === \"SUBSCRIBED\") {\n",
|
||||
" console.log(\n",
|
||||
" \"Conectado. Esperando cualquier cambio en la tabla asignaturas...\",\n",
|
||||
" );\n",
|
||||
" }\n",
|
||||
" if (err) {\n",
|
||||
" console.error(\"Error en el socket:\", err);\n",
|
||||
" }\n",
|
||||
" });\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"console.log(\"Script finalizado.\");\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3602b930",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import(\"https://esm.sh/@supabase/supabase-js\").then(({ createClient }) => {\n",
|
||||
" const supabase = createClient(\n",
|
||||
" \"https://bxkskdxwppdlplrkidcz.supabase.co\",\n",
|
||||
" \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a3NrZHh3cHBkbHBscmtpZGN6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIwMzU2NDMsImV4cCI6MjA4NzYxMTY0M30.RIyvmB8Ij_qrsiMn0f65awiG5VaCUA5dvb117wuU3jo\",\n",
|
||||
" );\n",
|
||||
"\n",
|
||||
" supabase.channel(\"prueba-consola\")\n",
|
||||
" .on(\"postgres_changes\", {\n",
|
||||
" event: \"*\",\n",
|
||||
" schema: \"public\",\n",
|
||||
" table: \"asignaturas\",\n",
|
||||
" }, (payload) => {\n",
|
||||
" console.log(\"🔥 ¡LLEGÓ EL EVENTO DESDE SUPABASE!\", payload);\n",
|
||||
" })\n",
|
||||
" .subscribe((status) => console.log(\"Estado de la conexión:\", status));\n",
|
||||
"});\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "26b3bb36",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"// plan Crear un plan en la base (sin datos) estado generando\n",
|
||||
"const { data: plan, error } = await supabase.from(\"planes_estudio\").insert({\n",
|
||||
" \"carrera_id\": \"8208da08-d549-4359-8865-9d806bc54f19\",\n",
|
||||
" \"estructura_id\": \"69fb2b77-5a95-47e0-bf1f-389d384200e4\",\n",
|
||||
" \"estado_actual_id\": \"18f49b67-8077-4371-be6e-2019a3be3562\",\n",
|
||||
" \"nombre\": \"Ingeniearía en Cifbernética y Ciberseguridad (2025)\",\n",
|
||||
" \"nivel\": \"Doctorado\",\n",
|
||||
" \"tipo_ciclo\": \"Semestre\",\n",
|
||||
" \"numero_ciclos\": 8,\n",
|
||||
" \"activo\": true,\n",
|
||||
" \"tipo_origen\": \"IA\",\n",
|
||||
" \"creado_por\": \"11111111-1111-1111-1111-111111111111\",\n",
|
||||
" \"actualizado_por\": \"11111111-1111-1111-1111-111111111111\",\n",
|
||||
"}).select(\"*\").single();\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8abb080e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" message: \u001b[32m\"TypeError: error sending request for url (http://127.0.0.1:54321/rest/v1/planes_estudio?select=*): client error (Connect): tcp connect error: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión. (os error 10061)\"\u001b[39m,\n",
|
||||
" details: \u001b[32m\"TypeError: error sending request for url (http://127.0.0.1:54321/rest/v1/planes_estudio?select=*): client error (Connect): tcp connect error: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión. (os error 10061)\\n\"\u001b[39m +\n",
|
||||
" \u001b[32m\" at async mainFetch (ext:deno_fetch/26_fetch.js:192:12)\\n\"\u001b[39m +\n",
|
||||
" \u001b[32m\" at async fetch (ext:deno_fetch/26_fetch.js:475:11)\\n\"\u001b[39m +\n",
|
||||
" \u001b[32m\" at async <anonymous>:2:31\"\u001b[39m,\n",
|
||||
" hint: \u001b[32m\"\"\u001b[39m,\n",
|
||||
" code: \u001b[32m\"\"\u001b[39m\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"plan.id;\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a382bdc6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" id: \u001b[32m\"resp_0ae72cbd0894279200699c8e42941c8192ba4a09e280b21a17\"\u001b[39m,\n",
|
||||
" object: \u001b[32m\"response\"\u001b[39m,\n",
|
||||
" created_at: \u001b[33m1771867714\u001b[39m,\n",
|
||||
" status: \u001b[32m\"queued\"\u001b[39m,\n",
|
||||
" background: \u001b[33mtrue\u001b[39m,\n",
|
||||
" completed_at: \u001b[1mnull\u001b[22m,\n",
|
||||
" error: \u001b[1mnull\u001b[22m,\n",
|
||||
" frequency_penalty: \u001b[33m0\u001b[39m,\n",
|
||||
" incomplete_details: \u001b[1mnull\u001b[22m,\n",
|
||||
" instructions: \u001b[1mnull\u001b[22m,\n",
|
||||
" max_output_tokens: \u001b[1mnull\u001b[22m,\n",
|
||||
" max_tool_calls: \u001b[1mnull\u001b[22m,\n",
|
||||
" model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n",
|
||||
" output: [],\n",
|
||||
" parallel_tool_calls: \u001b[33mtrue\u001b[39m,\n",
|
||||
" presence_penalty: \u001b[33m0\u001b[39m,\n",
|
||||
" previous_response_id: \u001b[1mnull\u001b[22m,\n",
|
||||
" prompt_cache_key: \u001b[1mnull\u001b[22m,\n",
|
||||
" prompt_cache_retention: \u001b[1mnull\u001b[22m,\n",
|
||||
" reasoning: { effort: \u001b[32m\"medium\"\u001b[39m, summary: \u001b[1mnull\u001b[22m },\n",
|
||||
" safety_identifier: \u001b[1mnull\u001b[22m,\n",
|
||||
" service_tier: \u001b[32m\"auto\"\u001b[39m,\n",
|
||||
" store: \u001b[33mtrue\u001b[39m,\n",
|
||||
" temperature: \u001b[33m1\u001b[39m,\n",
|
||||
" text: {\n",
|
||||
" format: {\n",
|
||||
" type: \u001b[32m\"json_schema\"\u001b[39m,\n",
|
||||
" description: \u001b[1mnull\u001b[22m,\n",
|
||||
" name: \u001b[32m\"response_schema\"\u001b[39m,\n",
|
||||
" schema: {\n",
|
||||
" type: \u001b[32m\"object\"\u001b[39m,\n",
|
||||
" properties: { \u001b[32m\"ai-message\"\u001b[39m: \u001b[36m[Object]\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[36m[Object]\u001b[39m },\n",
|
||||
" required: [ \u001b[32m\"ai-message\"\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m ],\n",
|
||||
" additionalProperties: \u001b[33mfalse\u001b[39m\n",
|
||||
" },\n",
|
||||
" strict: \u001b[33mtrue\u001b[39m\n",
|
||||
" },\n",
|
||||
" verbosity: \u001b[32m\"medium\"\u001b[39m\n",
|
||||
" },\n",
|
||||
" tool_choice: \u001b[32m\"auto\"\u001b[39m,\n",
|
||||
" tools: [],\n",
|
||||
" top_logprobs: \u001b[33m0\u001b[39m,\n",
|
||||
" top_p: \u001b[33m1\u001b[39m,\n",
|
||||
" truncation: \u001b[32m\"disabled\"\u001b[39m,\n",
|
||||
" usage: \u001b[1mnull\u001b[22m,\n",
|
||||
" user: \u001b[1mnull\u001b[22m,\n",
|
||||
" metadata: { tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"2\"\u001b[39m },\n",
|
||||
" output_text: \u001b[32m\"\"\u001b[39m\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"let response = await openai.responses.create({\n",
|
||||
" model: \"gpt-5-mini\",\n",
|
||||
" input: \"Hola mundo\",\n",
|
||||
" metadata: {\n",
|
||||
" tabla: \"planes_estudio\",\n",
|
||||
" id: '2',\n",
|
||||
" },\n",
|
||||
" text: {\n",
|
||||
" format: {\n",
|
||||
" type: \"json_schema\",\n",
|
||||
" name: \"response_schema\",\n",
|
||||
" schema: {\n",
|
||||
" type: \"object\",\n",
|
||||
" properties: {\n",
|
||||
" \"ai-message\": {\n",
|
||||
" type: \"string\",\n",
|
||||
" description:\n",
|
||||
" \"Un mensaje de texto que la IA generará para el usuario final. Este mensaje debe ser claro y directo, proporcionando la información solicitada por el usuario o indicando que no se puede procesar la solicitud si no está relacionada con planes de estudio o asignaturas.\",\n",
|
||||
" },\n",
|
||||
" \"suggested-topic\": {\n",
|
||||
" type: \"string\",\n",
|
||||
" description:\n",
|
||||
" \"Un tema de investigación sugerido por la IA que sea relevante para el plan de estudio o asignatura mencionada en la solicitud del usuario. Este tema debe ser específico y relacionado con el área de estudio correspondiente.\",\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" required: [\"ai-message\", \"suggested-topic\"],\n",
|
||||
" additionalProperties: false,\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" background: true,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"response;\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6b2470ed",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Current status: queued\n",
|
||||
"Current status: in_progress\n",
|
||||
"Current status: in_progress\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" id: \u001b[32m\"resp_0ae72cbd0894279200699c8e42941c8192ba4a09e280b21a17\"\u001b[39m,\n",
|
||||
" object: \u001b[32m\"response\"\u001b[39m,\n",
|
||||
" created_at: \u001b[33m1771867714\u001b[39m,\n",
|
||||
" status: \u001b[32m\"completed\"\u001b[39m,\n",
|
||||
" background: \u001b[33mtrue\u001b[39m,\n",
|
||||
" billing: { payer: \u001b[32m\"developer\"\u001b[39m },\n",
|
||||
" completed_at: \u001b[33m1771867721\u001b[39m,\n",
|
||||
" error: \u001b[1mnull\u001b[22m,\n",
|
||||
" frequency_penalty: \u001b[33m0\u001b[39m,\n",
|
||||
" incomplete_details: \u001b[1mnull\u001b[22m,\n",
|
||||
" instructions: \u001b[1mnull\u001b[22m,\n",
|
||||
" max_output_tokens: \u001b[1mnull\u001b[22m,\n",
|
||||
" max_tool_calls: \u001b[1mnull\u001b[22m,\n",
|
||||
" model: \u001b[32m\"gpt-5-mini-2025-08-07\"\u001b[39m,\n",
|
||||
" output: [\n",
|
||||
" {\n",
|
||||
" id: \u001b[32m\"rs_0ae72cbd0894279200699c8e43cd4c8192bcbc70792b6aa621\"\u001b[39m,\n",
|
||||
" type: \u001b[32m\"reasoning\"\u001b[39m,\n",
|
||||
" summary: []\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" id: \u001b[32m\"msg_0ae72cbd0894279200699c8e48976481928e1f205210636717\"\u001b[39m,\n",
|
||||
" type: \u001b[32m\"message\"\u001b[39m,\n",
|
||||
" status: \u001b[32m\"completed\"\u001b[39m,\n",
|
||||
" content: [\n",
|
||||
" {\n",
|
||||
" type: \u001b[32m\"output_text\"\u001b[39m,\n",
|
||||
" annotations: [],\n",
|
||||
" logprobs: [],\n",
|
||||
" text: \u001b[32m'{\"ai-message\":\"¡Hola! ¿En qué puedo ayudarte hoy? Puedo mostrar ejemplos de \\\\\"Hola mundo\\\\\" en distintos lenguajes, explicar conceptos de programación, ayudar con una tarea o sugerir recursos. Dime qué prefieres.\",\"suggested-topic\":\"El papel del programa \\\\\"Hola Mundo\\\\\" en la enseñanza de la programación: comparación práctica entre Python, Java y C (sintaxis, tiempo de arranque, facilidad de aprendizaje y percepción del estudiante).\"}'\u001b[39m\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" role: \u001b[32m\"assistant\"\u001b[39m\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" parallel_tool_calls: \u001b[33mtrue\u001b[39m,\n",
|
||||
" presence_penalty: \u001b[33m0\u001b[39m,\n",
|
||||
" previous_response_id: \u001b[1mnull\u001b[22m,\n",
|
||||
" prompt_cache_key: \u001b[1mnull\u001b[22m,\n",
|
||||
" prompt_cache_retention: \u001b[1mnull\u001b[22m,\n",
|
||||
" reasoning: { effort: \u001b[32m\"medium\"\u001b[39m, summary: \u001b[1mnull\u001b[22m },\n",
|
||||
" safety_identifier: \u001b[1mnull\u001b[22m,\n",
|
||||
" service_tier: \u001b[32m\"default\"\u001b[39m,\n",
|
||||
" store: \u001b[33mtrue\u001b[39m,\n",
|
||||
" temperature: \u001b[33m1\u001b[39m,\n",
|
||||
" text: {\n",
|
||||
" format: {\n",
|
||||
" type: \u001b[32m\"json_schema\"\u001b[39m,\n",
|
||||
" description: \u001b[1mnull\u001b[22m,\n",
|
||||
" name: \u001b[32m\"response_schema\"\u001b[39m,\n",
|
||||
" schema: {\n",
|
||||
" type: \u001b[32m\"object\"\u001b[39m,\n",
|
||||
" properties: { \u001b[32m\"ai-message\"\u001b[39m: \u001b[36m[Object]\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[36m[Object]\u001b[39m },\n",
|
||||
" required: [ \u001b[32m\"ai-message\"\u001b[39m, \u001b[32m\"suggested-topic\"\u001b[39m ],\n",
|
||||
" additionalProperties: \u001b[33mfalse\u001b[39m\n",
|
||||
" },\n",
|
||||
" strict: \u001b[33mtrue\u001b[39m\n",
|
||||
" },\n",
|
||||
" verbosity: \u001b[32m\"medium\"\u001b[39m\n",
|
||||
" },\n",
|
||||
" tool_choice: \u001b[32m\"auto\"\u001b[39m,\n",
|
||||
" tools: [],\n",
|
||||
" top_logprobs: \u001b[33m0\u001b[39m,\n",
|
||||
" top_p: \u001b[33m1\u001b[39m,\n",
|
||||
" truncation: \u001b[32m\"disabled\"\u001b[39m,\n",
|
||||
" usage: {\n",
|
||||
" input_tokens: \u001b[33m167\u001b[39m,\n",
|
||||
" input_tokens_details: { cached_tokens: \u001b[33m0\u001b[39m },\n",
|
||||
" output_tokens: \u001b[33m482\u001b[39m,\n",
|
||||
" output_tokens_details: { reasoning_tokens: \u001b[33m320\u001b[39m },\n",
|
||||
" total_tokens: \u001b[33m649\u001b[39m\n",
|
||||
" },\n",
|
||||
" user: \u001b[1mnull\u001b[22m,\n",
|
||||
" metadata: { tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"2\"\u001b[39m },\n",
|
||||
" output_text: \u001b[32m'{\"ai-message\":\"¡Hola! ¿En qué puedo ayudarte hoy? Puedo mostrar ejemplos de \\\\\"Hola mundo\\\\\" en distintos lenguajes, explicar conceptos de programación, ayudar con una tarea o sugerir recursos. Dime qué prefieres.\",\"suggested-topic\":\"El papel del programa \\\\\"Hola Mundo\\\\\" en la enseñanza de la programación: comparación práctica entre Python, Java y C (sintaxis, tiempo de arranque, facilidad de aprendizaje y percepción del estudiante).\"}'\u001b[39m\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"while (response.status === \"queued\" || response.status === \"in_progress\") {\n",
|
||||
" console.log(\"Current status: \" + response.status);\n",
|
||||
" await new Promise((resolve) => setTimeout(resolve, 2000)); // wait 2 seconds\n",
|
||||
" response = await openai.responses.retrieve(response.id);\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"response;\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ebb67bd8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{ tabla: \u001b[32m\"planes_estudio\"\u001b[39m, id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m }"
|
||||
]
|
||||
},
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response.metadata;\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cd18fc2d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" error: \u001b[1mnull\u001b[22m,\n",
|
||||
" data: {\n",
|
||||
" id: \u001b[32m\"e39952c1-a53b-43bf-8033-feda493a0651\"\u001b[39m,\n",
|
||||
" carrera_id: \u001b[32m\"8208da08-d549-4359-8865-9d806bc54f19\"\u001b[39m,\n",
|
||||
" estructura_id: \u001b[32m\"69fb2b77-5a95-47e0-bf1f-389d384200e4\"\u001b[39m,\n",
|
||||
" nombre: \u001b[32m\"Ingeniearía en Cifbernética y Ciberseguridad (2025)\"\u001b[39m,\n",
|
||||
" nivel: \u001b[32m\"Doctorado\"\u001b[39m,\n",
|
||||
" tipo_ciclo: \u001b[32m\"Semestre\"\u001b[39m,\n",
|
||||
" numero_ciclos: \u001b[33m8\u001b[39m,\n",
|
||||
" datos: {\n",
|
||||
" \u001b[32m\"ai-message\"\u001b[39m: \u001b[32m\"¡Hola! ¿Cómo puedo ayudarte hoy? Si te refieres al clásico “Hola mundo” en programación, puedo mostrarte ejemplos en varios lenguajes, explicar su historia o ayudarte a crear tu primer programa. ¿Qué prefieres?\"\u001b[39m,\n",
|
||||
" \u001b[32m\"suggested-topic\"\u001b[39m: \u001b[32m\"El uso del programa “Hola mundo” como herramienta pedagógica en la enseñanza de la programación: impacto en la comprensión inicial, variaciones por lenguaje y mejores prácticas didácticas.\"\u001b[39m\n",
|
||||
" },\n",
|
||||
" estado_actual_id: \u001b[32m\"18f49b67-8077-4371-be6e-2019a3be3562\"\u001b[39m,\n",
|
||||
" activo: \u001b[33mtrue\u001b[39m,\n",
|
||||
" tipo_origen: \u001b[32m\"IA\"\u001b[39m,\n",
|
||||
" meta_origen: {},\n",
|
||||
" creado_por: \u001b[32m\"11111111-1111-1111-1111-111111111111\"\u001b[39m,\n",
|
||||
" actualizado_por: \u001b[32m\"11111111-1111-1111-1111-111111111111\"\u001b[39m,\n",
|
||||
" creado_en: \u001b[32m\"2026-02-20T17:11:28.995222+00:00\"\u001b[39m,\n",
|
||||
" actualizado_en: \u001b[32m\"2026-02-20T17:12:40.170995+00:00\"\u001b[39m,\n",
|
||||
" nombre_search: \u001b[32m\"ingeniearia en cifbernetica y ciberseguridad (2025)\"\u001b[39m,\n",
|
||||
" plan_hash: \u001b[32m\"70d28457bbddfe4f5cde0574\"\u001b[39m\n",
|
||||
" },\n",
|
||||
" count: \u001b[1mnull\u001b[22m,\n",
|
||||
" status: \u001b[33m200\u001b[39m,\n",
|
||||
" statusText: \u001b[32m\"OK\"\u001b[39m\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 27,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await supabase.from(response.metadata.tabla).update({\n",
|
||||
" datos: JSON.parse(response.output_text),\n",
|
||||
"}).eq(\"id\", response.metadata.id).select(\"*\").single();\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cbbfd8c0",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "Expected ';', '}' or <eof> at file:///repl.tsx:2:93\n\n ...} ha sido actualizado con los datos generados por la IA.`: response.output_text,\n ~",
|
||||
"evalue": "Expected ';', '}' or <eof> at file:///repl.tsx:2:93\n\n ...} ha sido actualizado con los datos generados por la IA.`: response.output_text,\n ~",
|
||||
"output_type": "error",
|
||||
"traceback": []
|
||||
}
|
||||
],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a00c79bd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Deno",
|
||||
"language": "typescript",
|
||||
"name": "deno"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": "typescript",
|
||||
"file_extension": ".ts",
|
||||
"mimetype": "text/x.typescript",
|
||||
"name": "typescript",
|
||||
"nbconvert_exporter": "script",
|
||||
"pygments_lexer": "typescript",
|
||||
"version": "5.9.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "902bff53",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { load } from \"jsr:@std/dotenv\";\n",
|
||||
"import OpenAI from \"jsr:@openai/openai\";\n",
|
||||
"\n",
|
||||
"const env = await load({\n",
|
||||
" export: true,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"const openai = new OpenAI();"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "68f89168",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"const perishables = {\n",
|
||||
" expiryDate: { type: \"string\", format: \"date\" },\n",
|
||||
" batchNumber: {\n",
|
||||
" type: \"string\",\n",
|
||||
" maxLength: 20,\n",
|
||||
" description: \"Puede ser inventado\",\n",
|
||||
" },\n",
|
||||
" storageTempC: {\n",
|
||||
" type: \"number\",\n",
|
||||
" minimum: -30,\n",
|
||||
" maximum: 10,\n",
|
||||
" description: \"Temperatura de almacenamiento en grados Celsius\",\n",
|
||||
" },\n",
|
||||
"};\n",
|
||||
"\n",
|
||||
"const origen = {\n",
|
||||
" country: { type: \"string\", maxLength: 50 },\n",
|
||||
" city: { type: \"string\", maxLength: 50 },\n",
|
||||
"};\n",
|
||||
"\n",
|
||||
"// Transformar las definiciones: poner en properties, agregar required, y additionalProperties: false\n",
|
||||
"\n",
|
||||
"const perishablesWithMetadata = {\n",
|
||||
" ...perishables,\n",
|
||||
" type: \"object\",\n",
|
||||
" required: Object.keys(perishables),\n",
|
||||
" additionalProperties: false,\n",
|
||||
"};\n",
|
||||
"\n",
|
||||
"const origenWithMetadata = {\n",
|
||||
" ...origen,\n",
|
||||
" type: \"object\",\n",
|
||||
" required: Object.keys(origen),\n",
|
||||
" additionalProperties: false,\n",
|
||||
"};\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"const combinedSchema = {\n",
|
||||
" type: \"object\",\n",
|
||||
" $defs: {\n",
|
||||
" perishables: perishablesWithMetadata,\n",
|
||||
" origen: origenWithMetadata,\n",
|
||||
" },\n",
|
||||
" properties: {\n",
|
||||
" nombre: { type: \"string\", maxLength: 100 },\n",
|
||||
" fabricante: { type: \"string\", maxLength: 100 },\n",
|
||||
"\n",
|
||||
" // Agregar campo del preset\n",
|
||||
" perishables: { $ref: \"#/$defs/perishables\" },\n",
|
||||
" origen: { $ref: \"#/$defs/origen\" },\n",
|
||||
" },\n",
|
||||
" required: [\"nombre\", \"fabricante\", \"perishables\", \"origen\"],\n",
|
||||
" additionalProperties: false,\n",
|
||||
"};\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1403c789",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { title } from \"node:process\";\n",
|
||||
"\n",
|
||||
"const combinedSchema = {\n",
|
||||
" type: \"object\",\n",
|
||||
" $defs: {\n",
|
||||
" perishables: {\n",
|
||||
" type: \"object\",\n",
|
||||
" properties: {\n",
|
||||
" expiryDate: { type: \"string\", format: \"date\" },\n",
|
||||
" batchNumber: {\n",
|
||||
" type: \"string\",\n",
|
||||
" maxLength: 20,\n",
|
||||
" description: \"Puede ser inventado\",\n",
|
||||
" },\n",
|
||||
" storageTempC: {\n",
|
||||
" type: \"number\",\n",
|
||||
" minimum: -30,\n",
|
||||
" maximum: 10,\n",
|
||||
" description:\n",
|
||||
" \"Temperatura de almacenamiento en grados Celsius\",\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" required: [\"expiryDate\", \"batchNumber\", \"storageTempC\"],\n",
|
||||
" additionalProperties: false,\n",
|
||||
" },\n",
|
||||
" origen: {\n",
|
||||
" type: \"object\",\n",
|
||||
" properties: {\n",
|
||||
" country: { type: \"string\", maxLength: 50 },\n",
|
||||
" city: { type: \"string\", maxLength: 50 },\n",
|
||||
" },\n",
|
||||
" required: [\"country\", \"city\"],\n",
|
||||
" additionalProperties: false,\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" properties: {\n",
|
||||
" nombre: { type: \"string\", maxLength: 100 },\n",
|
||||
" fabricante: { type: \"string\", maxLength: 100 },\n",
|
||||
"\n",
|
||||
" // Agregar campo del preset\n",
|
||||
" perishables: {\n",
|
||||
" $ref: \"#/$defs/perishables\",\n",
|
||||
" \"x-column\": \"perishables\",\n",
|
||||
" },\n",
|
||||
"\n",
|
||||
" origen: { $ref: \"#/$defs/origen\" },\n",
|
||||
" },\n",
|
||||
" required: [\"nombre\", \"fabricante\", \"perishables\", \"origen\", \"perecedero\"],\n",
|
||||
" additionalProperties: false,\n",
|
||||
"};\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 27,
|
||||
"id": "1fbda124",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"const result = await openai.responses.create({\n",
|
||||
" model: \"gpt-5-nano\",\n",
|
||||
" input:\n",
|
||||
" \"Genera un producto con el siguiente formato JSON, sin texto adicional:\",\n",
|
||||
" text: {\n",
|
||||
" format: {\n",
|
||||
" type: \"json_schema\",\n",
|
||||
" name: \"producto\",\n",
|
||||
" schema: combinedSchema,\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
"});\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 28,
|
||||
"id": "85e7967a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" nombre: \u001b[32m\"Galletas de avena premium\"\u001b[39m,\n",
|
||||
" fabricante: \u001b[32m\"Industrias Gourmet S.L.\"\u001b[39m,\n",
|
||||
" perishables: {\n",
|
||||
" expiryDate: \u001b[32m\"2026-12-31\"\u001b[39m,\n",
|
||||
" batchNumber: \u001b[32m\"BATCH-00123\"\u001b[39m,\n",
|
||||
" storageTempC: \u001b[33m4\u001b[39m\n",
|
||||
" },\n",
|
||||
" origen: { country: \u001b[32m\"España\"\u001b[39m, city: \u001b[32m\"Valencia\"\u001b[39m },\n",
|
||||
" perecedero: \u001b[1mnull\u001b[22m\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 28,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"JSON.parse(result.output_text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "dae842ad",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Deno",
|
||||
"language": "typescript",
|
||||
"name": "deno"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": "typescript",
|
||||
"file_extension": ".ts",
|
||||
"mimetype": "text/x.typescript",
|
||||
"name": "typescript",
|
||||
"nbconvert_exporter": "script",
|
||||
"pygments_lexer": "typescript",
|
||||
"version": "5.9.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "902bff53",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { load } from \"jsr:@std/dotenv\";\n",
|
||||
"import OpenAI from \"jsr:@openai/openai\";\n",
|
||||
"\n",
|
||||
"const env = await load({\n",
|
||||
" export: true,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"const openai = new OpenAI();"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "3b0acbc3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { createClient } from \"@supabase/supabase-js\";\n",
|
||||
"const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n",
|
||||
"const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n",
|
||||
"const supabase = createClient(supabaseUrl, supabaseKey);"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "dae842ad",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "ReferenceError",
|
||||
"evalue": "openai is not defined",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"Stack trace:",
|
||||
"ReferenceError: openai is not defined",
|
||||
" at <anonymous>:5:15"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"const textos = [\n",
|
||||
" \"Los vectores se usan con el cliente de OPENAI embeddings\",\n",
|
||||
"];\n",
|
||||
"\n",
|
||||
"for (const contenido of textos) {\n",
|
||||
" const emb = await openai.embeddings.create({\n",
|
||||
" model: \"text-embedding-3-small\",\n",
|
||||
" input: contenido,\n",
|
||||
" });\n",
|
||||
"\n",
|
||||
" const embedding = emb.data[0].embedding;\n",
|
||||
" console.log(emb);\n",
|
||||
" \n",
|
||||
"\n",
|
||||
" const { error } = await supabase.from(\"documentos\").insert({\n",
|
||||
" contenido,\n",
|
||||
" embedding,\n",
|
||||
" });\n",
|
||||
"\n",
|
||||
" if (error) throw error;\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"console.log(\"✅ Insertados textos con embeddings\");"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "2d8fa9a9",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔎 Consulta: ¿Cómo son buscados vectores?\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"\u001b[32m\"code: PGRST202\\n\"\u001b[39m +\n",
|
||||
" \u001b[32m'details: \"Searched for the function public.buscar_documentos with parameters match_count, query_embedding or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.\"\\n'\u001b[39m +\n",
|
||||
" \u001b[32m\"hint: Perhaps you meant to call the function public.unaccent\\n\"\u001b[39m +\n",
|
||||
" \u001b[32m'message: \"Could not find the function public.buscar_documentos(match_count, query_embedding) in the schema cache\"'\u001b[39m"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import { encode } from \"@toon-format/toon\";\n",
|
||||
"\n",
|
||||
"const consulta = \"¿Cómo son buscados vectores?\";\n",
|
||||
"\n",
|
||||
"const emb = await openai.embeddings.create({\n",
|
||||
" model: \"text-embedding-3-small\",\n",
|
||||
" input: consulta,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"const query_embedding = emb.data[0].embedding;\n",
|
||||
"\n",
|
||||
"const { data, error } = await supabase.rpc(\"buscar_documentos\", {\n",
|
||||
" query_embedding,\n",
|
||||
" match_count: 3,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"/* if (error) throw error; */\n",
|
||||
"\n",
|
||||
"console.log(`🔎 Consulta: ${consulta}\\n`);\n",
|
||||
"encode(error, {\n",
|
||||
" indent: 2,\n",
|
||||
" delimiter: \",\",\n",
|
||||
" keyFolding: \"off\",\n",
|
||||
" flattenDepth: Infinity,\n",
|
||||
"});\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "791a94d8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" code: \u001b[32m\"PGRST202\"\u001b[39m,\n",
|
||||
" details: \u001b[32m\"Searched for the function public.buscar_documentos with parameters match_count, query_embedding or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache.\"\u001b[39m,\n",
|
||||
" hint: \u001b[32m\"Perhaps you meant to call the function public.unaccent\"\u001b[39m,\n",
|
||||
" message: \u001b[32m\"Could not find the function public.buscar_documentos(match_count, query_embedding) in the schema cache\"\u001b[39m\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Deno",
|
||||
"language": "typescript",
|
||||
"name": "deno"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": "typescript",
|
||||
"file_extension": ".ts",
|
||||
"mimetype": "text/x.typescript",
|
||||
"name": "typescript",
|
||||
"nbconvert_exporter": "script",
|
||||
"pygments_lexer": "typescript",
|
||||
"version": "5.9.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,519 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" demote-non-dropping-particle="sort-only">
|
||||
<info>
|
||||
<title>IEEE Reference Guide version 11.29.2023</title>
|
||||
<title-short>Institute of Electrical and Electronics Engineers</title-short>
|
||||
<id>http://www.zotero.org/styles/ieee</id>
|
||||
<link href="http://www.zotero.org/styles/ieee" rel="self"/>
|
||||
<link href="https://journals.ieeeauthorcenter.ieee.org/your-role-in-article-production/ieee-editorial-style-manual/" rel="documentation"/>
|
||||
<author>
|
||||
<name>Michael Berkowitz</name>
|
||||
<email>mberkowi@gmu.edu</email>
|
||||
</author>
|
||||
<contributor>
|
||||
<name>Julian Onions</name>
|
||||
<email>julian.onions@gmail.com</email>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Rintze Zelle</name>
|
||||
<uri>http://twitter.com/rintzezelle</uri>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Stephen Frank</name>
|
||||
<uri>http://www.zotero.org/sfrank</uri>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Sebastian Karcher</name>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Giuseppe Silano</name>
|
||||
<email>g.silano89@gmail.com</email>
|
||||
<uri>http://giuseppesilano.net</uri>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Patrick O'Brien</name>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Brenton M. Wiernik</name>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Oliver Couch</name>
|
||||
<email>oliver.couch@gmail.com</email>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Andrew Dunning</name>
|
||||
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
||||
</contributor>
|
||||
<category citation-format="numeric"/>
|
||||
<category field="engineering"/>
|
||||
<category field="generic-base"/>
|
||||
<summary>IEEE style as per the 2023 guidelines.</summary>
|
||||
<updated>2024-03-27T11:41:27+00:00</updated>
|
||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||
</info>
|
||||
<locale xml:lang="en">
|
||||
<date form="text">
|
||||
<date-part name="month" form="short" suffix=" "/>
|
||||
<date-part name="day" form="numeric-leading-zeros" suffix=", "/>
|
||||
<date-part name="year"/>
|
||||
</date>
|
||||
<terms>
|
||||
<term name="chapter" form="short">ch.</term>
|
||||
<term name="chapter-number" form="short">ch.</term>
|
||||
<term name="presented at">presented at the</term>
|
||||
<term name="available at">available</term>
|
||||
<!-- always use three-letter abbreviations for months -->
|
||||
<term name="month-06" form="short">Jun.</term>
|
||||
<term name="month-07" form="short">Jul.</term>
|
||||
<term name="month-09" form="short">Sep.</term>
|
||||
</terms>
|
||||
</locale>
|
||||
<!-- Macros -->
|
||||
<macro name="status">
|
||||
<choose>
|
||||
<if variable="page issue volume" match="none">
|
||||
<text variable="status" text-case="capitalize-first" suffix="" font-weight="bold"/>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="edition">
|
||||
<choose>
|
||||
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference report song" match="any">
|
||||
<choose>
|
||||
<if is-numeric="edition">
|
||||
<group delimiter=" ">
|
||||
<number variable="edition" form="ordinal"/>
|
||||
<text term="edition" form="short"/>
|
||||
</group>
|
||||
</if>
|
||||
<else>
|
||||
<text variable="edition" text-case="capitalize-first" suffix="."/>
|
||||
</else>
|
||||
</choose>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="issued">
|
||||
<choose>
|
||||
<if type="article-journal report" match="any">
|
||||
<date variable="issued">
|
||||
<date-part name="month" form="short" suffix=" "/>
|
||||
<date-part name="year" form="long"/>
|
||||
</date>
|
||||
</if>
|
||||
<else-if type="bill book chapter graphic legal_case legislation song thesis" match="any">
|
||||
<date variable="issued">
|
||||
<date-part name="year" form="long"/>
|
||||
</date>
|
||||
</else-if>
|
||||
<else-if type="paper-conference" match="any">
|
||||
<date variable="issued">
|
||||
<date-part name="month" form="short"/>
|
||||
<date-part name="year" prefix=" "/>
|
||||
</date>
|
||||
</else-if>
|
||||
<else-if type="motion_picture" match="any">
|
||||
<date variable="issued" form="text" prefix="(" suffix=")"/>
|
||||
</else-if>
|
||||
<else>
|
||||
<date variable="issued" form="text"/>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="author">
|
||||
<names variable="author">
|
||||
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
|
||||
<label form="short" prefix=", " text-case="capitalize-first"/>
|
||||
<et-al font-style="italic"/>
|
||||
<substitute>
|
||||
<names variable="editor"/>
|
||||
<names variable="translator"/>
|
||||
<text macro="director"/>
|
||||
</substitute>
|
||||
</names>
|
||||
</macro>
|
||||
<macro name="editor">
|
||||
<names variable="editor">
|
||||
<name initialize-with=". " delimiter=", " and="text"/>
|
||||
<label form="short" prefix=", " text-case="capitalize-first"/>
|
||||
</names>
|
||||
</macro>
|
||||
<macro name="director">
|
||||
<names variable="director">
|
||||
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
|
||||
<et-al font-style="italic"/>
|
||||
</names>
|
||||
</macro>
|
||||
<macro name="locators">
|
||||
<group delimiter=", ">
|
||||
<text macro="edition"/>
|
||||
<group delimiter=" ">
|
||||
<text term="volume" form="short"/>
|
||||
<number variable="volume" form="numeric"/>
|
||||
</group>
|
||||
<group delimiter=" ">
|
||||
<number variable="number-of-volumes" form="numeric"/>
|
||||
<text term="volume" form="short" plural="true"/>
|
||||
</group>
|
||||
<group delimiter=" ">
|
||||
<text term="issue" form="short"/>
|
||||
<number variable="issue" form="numeric"/>
|
||||
</group>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="title">
|
||||
<choose>
|
||||
<if type="bill book graphic legal_case legislation motion_picture song standard software" match="any">
|
||||
<text variable="title" font-style="italic"/>
|
||||
</if>
|
||||
<else>
|
||||
<text variable="title" quotes="true"/>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="publisher">
|
||||
<choose>
|
||||
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference song" match="any">
|
||||
<group delimiter=": ">
|
||||
<text variable="publisher-place"/>
|
||||
<text variable="publisher"/>
|
||||
</group>
|
||||
</if>
|
||||
<else>
|
||||
<group delimiter=", ">
|
||||
<text variable="publisher"/>
|
||||
<text variable="publisher-place"/>
|
||||
</group>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="event">
|
||||
<choose>
|
||||
<!-- Published Conference Paper -->
|
||||
<if type="paper-conference speech" match="any">
|
||||
<choose>
|
||||
<if variable="container-title" match="any">
|
||||
<group delimiter=" ">
|
||||
<text term="in"/>
|
||||
<text variable="container-title" font-style="italic"/>
|
||||
</group>
|
||||
</if>
|
||||
<!-- Unpublished Conference Paper -->
|
||||
<else>
|
||||
<group delimiter=" ">
|
||||
<text term="presented at"/>
|
||||
<text variable="event"/>
|
||||
</group>
|
||||
</else>
|
||||
</choose>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="access">
|
||||
<choose>
|
||||
<if type="webpage post post-weblog" match="any">
|
||||
<!-- https://url.com/ (accessed Mon. DD, YYYY). -->
|
||||
<choose>
|
||||
<if variable="URL">
|
||||
<group delimiter=". " prefix=" ">
|
||||
<group delimiter=": ">
|
||||
<text term="accessed" text-case="capitalize-first"/>
|
||||
<date variable="accessed" form="text"/>
|
||||
</group>
|
||||
<text term="online" prefix="[" suffix="]" text-case="capitalize-first"/>
|
||||
<group delimiter=": ">
|
||||
<text term="available at" text-case="capitalize-first"/>
|
||||
<text variable="URL"/>
|
||||
</group>
|
||||
</group>
|
||||
</if>
|
||||
</choose>
|
||||
</if>
|
||||
<else-if match="any" variable="DOI">
|
||||
<!-- doi: 10.1000/xyz123. -->
|
||||
<text variable="DOI" prefix=" doi: " suffix="."/>
|
||||
</else-if>
|
||||
<else-if variable="URL">
|
||||
<!-- Accessed: Mon. DD, YYYY. [Medium]. Available: https://URL.com/ -->
|
||||
<group delimiter=". " prefix=" " suffix=". ">
|
||||
<!-- Accessed: Mon. DD, YYYY. -->
|
||||
<group delimiter=": ">
|
||||
<text term="accessed" text-case="capitalize-first"/>
|
||||
<date variable="accessed" form="text"/>
|
||||
</group>
|
||||
<!-- [Online Video]. -->
|
||||
<group prefix="[" suffix="]" delimiter=" ">
|
||||
<choose>
|
||||
<if variable="medium" match="any">
|
||||
<text variable="medium" text-case="capitalize-first"/>
|
||||
</if>
|
||||
<else>
|
||||
<text term="online" text-case="capitalize-first"/>
|
||||
<choose>
|
||||
<if type="motion_picture">
|
||||
<text term="video" text-case="capitalize-first"/>
|
||||
</if>
|
||||
</choose>
|
||||
</else>
|
||||
</choose>
|
||||
</group>
|
||||
</group>
|
||||
<!-- Available: https://URL.com/ -->
|
||||
<group delimiter=": " prefix=" ">
|
||||
<text term="available at" text-case="capitalize-first"/>
|
||||
<text variable="URL"/>
|
||||
</group>
|
||||
</else-if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="page">
|
||||
<choose>
|
||||
<if type="article-journal" variable="number" match="all">
|
||||
<group delimiter=" ">
|
||||
<text value="Art."/>
|
||||
<text term="issue" form="short"/>
|
||||
<text variable="number"/>
|
||||
</group>
|
||||
</if>
|
||||
<else>
|
||||
<group delimiter=" ">
|
||||
<label variable="page" form="short"/>
|
||||
<text variable="page"/>
|
||||
</group>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="citation-locator">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if locator="page">
|
||||
<label variable="locator" form="short"/>
|
||||
</if>
|
||||
<else>
|
||||
<label variable="locator" form="short" text-case="capitalize-first"/>
|
||||
</else>
|
||||
</choose>
|
||||
<text variable="locator"/>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="geographic-location">
|
||||
<group delimiter=", " suffix=".">
|
||||
<choose>
|
||||
<if variable="publisher-place">
|
||||
<text variable="publisher-place" text-case="title"/>
|
||||
</if>
|
||||
<else-if variable="event-place">
|
||||
<text variable="event-place" text-case="title"/>
|
||||
</else-if>
|
||||
</choose>
|
||||
</group>
|
||||
</macro>
|
||||
<!-- Series -->
|
||||
<macro name="collection">
|
||||
<choose>
|
||||
<if variable="collection-title" match="any">
|
||||
<text term="in" suffix=" "/>
|
||||
<group delimiter=", " suffix=". ">
|
||||
<text variable="collection-title"/>
|
||||
<text variable="collection-number" prefix="no. "/>
|
||||
<text variable="volume" prefix="vol. "/>
|
||||
</group>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<!-- Citation -->
|
||||
<citation>
|
||||
<sort>
|
||||
<key variable="citation-number"/>
|
||||
</sort>
|
||||
<layout delimiter=", ">
|
||||
<group prefix="[" suffix="]" delimiter=", ">
|
||||
<text variable="citation-number"/>
|
||||
<text macro="citation-locator"/>
|
||||
</group>
|
||||
</layout>
|
||||
</citation>
|
||||
<!-- Bibliography -->
|
||||
<bibliography entry-spacing="0" second-field-align="flush">
|
||||
<layout>
|
||||
<!-- Citation Number -->
|
||||
<text variable="citation-number" prefix="[" suffix="]"/>
|
||||
<!-- Author(s) -->
|
||||
<text macro="author" suffix=", "/>
|
||||
<!-- Rest of Citation -->
|
||||
<choose>
|
||||
<!-- Specific Formats -->
|
||||
<if type="article-journal">
|
||||
<group delimiter=", ">
|
||||
<text macro="title"/>
|
||||
<text variable="container-title" font-style="italic" form="short"/>
|
||||
<text macro="locators"/>
|
||||
<text macro="page"/>
|
||||
<text macro="issued"/>
|
||||
<text macro="status"/>
|
||||
</group>
|
||||
<choose>
|
||||
<if variable="URL DOI" match="none">
|
||||
<text value="."/>
|
||||
</if>
|
||||
<else>
|
||||
<text value=","/>
|
||||
</else>
|
||||
</choose>
|
||||
<text macro="access"/>
|
||||
</if>
|
||||
<else-if type="paper-conference speech" match="any">
|
||||
<group delimiter=", " suffix=", ">
|
||||
<text macro="title"/>
|
||||
<text macro="event"/>
|
||||
<text macro="editor"/>
|
||||
</group>
|
||||
<text macro="collection"/>
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="publisher"/>
|
||||
<text macro="issued"/>
|
||||
<text macro="page"/>
|
||||
<text macro="status"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="chapter">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<group delimiter=" ">
|
||||
<text term="in" suffix=" "/>
|
||||
<text variable="container-title" font-style="italic"/>
|
||||
</group>
|
||||
<text macro="locators"/>
|
||||
<text macro="editor"/>
|
||||
<text macro="collection"/>
|
||||
<text macro="publisher"/>
|
||||
<text macro="issued"/>
|
||||
<group delimiter=" ">
|
||||
<label variable="chapter-number" form="short"/>
|
||||
<text variable="chapter-number"/>
|
||||
</group>
|
||||
<text macro="page"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="report">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text macro="publisher"/>
|
||||
<group delimiter=" ">
|
||||
<text variable="genre"/>
|
||||
<text variable="number"/>
|
||||
</group>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="thesis">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text variable="genre"/>
|
||||
<text macro="publisher"/>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="software">
|
||||
<group delimiter=". " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text macro="issued" prefix="(" suffix=")"/>
|
||||
<text variable="genre"/>
|
||||
<text macro="publisher"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="article">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text macro="issued"/>
|
||||
<group delimiter=": ">
|
||||
<text macro="publisher" font-style="italic"/>
|
||||
<text variable="number"/>
|
||||
</group>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="webpage post-weblog post" match="any">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text variable="container-title"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="patent">
|
||||
<group delimiter=", ">
|
||||
<text macro="title"/>
|
||||
<text variable="number"/>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<!-- Online Video -->
|
||||
<else-if type="motion_picture">
|
||||
<text macro="geographic-location" suffix=". "/>
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="standard">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<group delimiter=" ">
|
||||
<text variable="genre"/>
|
||||
<text variable="number"/>
|
||||
</group>
|
||||
<text macro="geographic-location"/>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<!-- Generic/Fallback Formats -->
|
||||
<else-if type="bill book graphic legal_case legislation report song" match="any">
|
||||
<group delimiter=", " suffix=". ">
|
||||
<text macro="title"/>
|
||||
<text macro="locators"/>
|
||||
</group>
|
||||
<text macro="collection"/>
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="publisher"/>
|
||||
<text macro="issued"/>
|
||||
<text macro="page"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else-if type="article-magazine article-newspaper broadcast interview manuscript map patent personal_communication song speech thesis webpage" match="any">
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="title"/>
|
||||
<text variable="container-title" font-style="italic"/>
|
||||
<text macro="locators"/>
|
||||
<text macro="publisher"/>
|
||||
<text macro="page"/>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else-if>
|
||||
<else>
|
||||
<group delimiter=", " suffix=". ">
|
||||
<text macro="title"/>
|
||||
<text variable="container-title" font-style="italic"/>
|
||||
<text macro="locators"/>
|
||||
</group>
|
||||
<text macro="collection"/>
|
||||
<group delimiter=", " suffix=".">
|
||||
<text macro="publisher"/>
|
||||
<text macro="page"/>
|
||||
<text macro="issued"/>
|
||||
</group>
|
||||
<text macro="access"/>
|
||||
</else>
|
||||
</choose>
|
||||
</layout>
|
||||
</bibliography>
|
||||
</style>
|
||||
@@ -0,0 +1,757 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale xmlns="http://purl.org/net/xbiblio/csl" version="1.0" xml:lang="es-MX">
|
||||
<info>
|
||||
<translator>
|
||||
<name>Juan Ignacio Flores Salgado</name>
|
||||
<uri>https://www.mendeley.com/profiles/juan-ignacio-flores-salgado/</uri>
|
||||
</translator>
|
||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||
<updated>2025-10-16T03:24:00+00:00</updated>
|
||||
</info>
|
||||
<style-options punctuation-in-quote="false"/>
|
||||
<date form="text">
|
||||
<date-part name="day" prefix="el " suffix=" de "/>
|
||||
<date-part name="month" suffix=" de "/>
|
||||
<date-part name="year"/>
|
||||
</date>
|
||||
<date form="numeric">
|
||||
<date-part name="day" form="numeric-leading-zeros" suffix="/"/>
|
||||
<date-part name="month" form="numeric-leading-zeros" suffix="/"/>
|
||||
<date-part name="year"/>
|
||||
</date>
|
||||
<terms>
|
||||
<!-- LONG GENERAL TERMS -->
|
||||
<term name="accessed">consultado</term>
|
||||
<term name="advance-online-publication">advance online publication</term>
|
||||
<term name="album">album</term>
|
||||
<term name="and">y</term>
|
||||
<term name="and others">et al.</term>
|
||||
<term name="anonymous">anónimo</term>
|
||||
<term name="at">en</term>
|
||||
<term name="audio-recording">audio recording</term>
|
||||
<term name="available at">disponible en</term>
|
||||
<term name="by">de</term>
|
||||
<term name="circa">circa</term>
|
||||
<term name="cited">citado</term>
|
||||
<term name="et-al">et al.</term>
|
||||
<term name="film">film</term>
|
||||
<term name="forthcoming">en preparación</term>
|
||||
<term name="from">a partir de</term>
|
||||
<term name="henceforth">henceforth</term>
|
||||
<term name="ibid">ibid.</term>
|
||||
<term name="in">en</term>
|
||||
<term name="in press">en imprenta</term>
|
||||
<term name="internet">internet</term>
|
||||
<term name="letter">carta</term>
|
||||
<term name="loc-cit">loc. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
||||
<term name="no date">sin fecha</term>
|
||||
<term name="no-place">no place</term>
|
||||
<term name="no-publisher">no publisher</term> <!-- sine nomine -->
|
||||
<term name="on">on</term>
|
||||
<term name="online">en línea</term>
|
||||
<term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
||||
<term name="original-work-published">obra original publicada en</term>
|
||||
<term name="personal-communication">comunicación personal</term>
|
||||
<term name="podcast">podcast</term>
|
||||
<term name="podcast-episode">podcast episode</term>
|
||||
<term name="preprint">preprint</term>
|
||||
<term name="presented at">presentado en</term>
|
||||
<term name="radio-broadcast">radio broadcast</term>
|
||||
<term name="radio-series">radio series</term>
|
||||
<term name="radio-series-episode">radio series episode</term>
|
||||
<term name="reference">
|
||||
<single>referencia</single>
|
||||
<multiple>referencias</multiple>
|
||||
</term>
|
||||
<term name="retrieved">recuperado</term>
|
||||
<term name="review-of">review of</term>
|
||||
<term name="scale">escala</term>
|
||||
<term name="special-issue">special issue</term>
|
||||
<term name="special-section">special section</term>
|
||||
<term name="television-broadcast">television broadcast</term>
|
||||
<term name="television-series">television series</term>
|
||||
<term name="television-series-episode">television series episode</term>
|
||||
<term name="video">video</term>
|
||||
<term name="working-paper">working paper</term>
|
||||
|
||||
<!-- SHORT GENERAL TERMS -->
|
||||
<term name="anonymous" form="short">anón.</term>
|
||||
<term name="circa" form="short">c.</term>
|
||||
<term name="no date" form="short">s/f</term>
|
||||
<term name="no-place" form="short">n.p.</term>
|
||||
<term name="no-publisher" form="short">n.p.</term>
|
||||
<term name="reference" form="short">
|
||||
<single>ref.</single>
|
||||
<multiple>refs.</multiple>
|
||||
</term>
|
||||
<term name="review-of" form="short">rev. of</term>
|
||||
|
||||
<!-- SYMBOLIC GENERAL FORMS -->
|
||||
|
||||
<!-- LONG ITEM TYPE FORMS -->
|
||||
<term name="article">preprint</term>
|
||||
<term name="article-journal">journal article</term>
|
||||
<term name="article-magazine">magazine article</term>
|
||||
<term name="article-newspaper">newspaper article</term>
|
||||
<term name="bill">bill</term>
|
||||
<!-- book is in the list of locator terms -->
|
||||
<term name="broadcast">broadcast</term>
|
||||
<!-- chapter is in the list of locator terms -->
|
||||
<term name="classic">classic</term>
|
||||
<term name="collection">collection</term>
|
||||
<term name="dataset">dataset</term>
|
||||
<term name="document">document</term>
|
||||
<term name="entry">entry</term>
|
||||
<term name="entry-dictionary">dictionary entry</term>
|
||||
<term name="entry-encyclopedia">encyclopedia entry</term>
|
||||
<term name="event">event</term>
|
||||
<!-- figure is in the list of locator terms -->
|
||||
<term name="graphic">graphic</term>
|
||||
<term name="hearing">hearing</term>
|
||||
<term name="interview">entrevista</term>
|
||||
<term name="legal_case">legal case</term>
|
||||
<term name="legislation">legislation</term>
|
||||
<term name="manuscript">manuscript</term>
|
||||
<term name="map">map</term>
|
||||
<term name="motion_picture">video recording</term>
|
||||
<term name="musical_score">musical score</term>
|
||||
<term name="pamphlet">pamphlet</term>
|
||||
<term name="paper-conference">conference paper</term>
|
||||
<term name="patent">patent</term>
|
||||
<term name="performance">performance</term>
|
||||
<term name="periodical">periodical</term>
|
||||
<term name="personal_communication">comunicación personal</term>
|
||||
<term name="post">post</term>
|
||||
<term name="post-weblog">blog post</term>
|
||||
<term name="regulation">regulation</term>
|
||||
<term name="report">report</term>
|
||||
<term name="review">review</term>
|
||||
<term name="review-book">book review</term>
|
||||
<term name="software">software</term>
|
||||
<term name="song">audio recording</term>
|
||||
<term name="speech">presentation</term>
|
||||
<term name="standard">standard</term>
|
||||
<term name="thesis">thesis</term>
|
||||
<term name="treaty">treaty</term>
|
||||
<term name="webpage">webpage</term>
|
||||
|
||||
<!-- SHORT ITEM TYPE FORMS -->
|
||||
<term name="article-journal" form="short">journal art.</term>
|
||||
<term name="article-magazine" form="short">mag. art.</term>
|
||||
<term name="article-newspaper" form="short">newspaper art.</term>
|
||||
<!-- book is in the list of locator terms -->
|
||||
<!-- chapter is in the list of locator terms -->
|
||||
<term name="document" form="short">doc.</term>
|
||||
<!-- figure is in the list of locator terms -->
|
||||
<term name="graphic" form="short">graph.</term>
|
||||
<term name="interview" form="short">interv.</term>
|
||||
<term name="manuscript" form="short">MS</term>
|
||||
<term name="motion_picture" form="short">video rec.</term>
|
||||
<term name="report" form="short">rep.</term>
|
||||
<term name="review" form="short">rev.</term>
|
||||
<term name="review-book" form="short">bk. rev.</term>
|
||||
<term name="song" form="short">audio rec.</term>
|
||||
|
||||
<!-- LONG VERB ITEM TYPE FORMS -->
|
||||
<!-- Only where applicable -->
|
||||
<term name="hearing" form="verb">testimony of</term>
|
||||
<term name="review" form="verb">review of</term>
|
||||
<term name="review-book" form="verb">review of the book</term>
|
||||
|
||||
<!-- SHORT VERB ITEM TYPE FORMS -->
|
||||
|
||||
<!-- HISTORICAL ERA TERMS -->
|
||||
<term name="ad">d. C.</term>
|
||||
<term name="bc">a. C.</term>
|
||||
<term name="bce">BCE</term>
|
||||
<term name="ce">CE</term>
|
||||
|
||||
<!-- PUNCTUATION -->
|
||||
<term name="open-quote">“</term>
|
||||
<term name="close-quote">”</term>
|
||||
<term name="open-inner-quote">‘</term>
|
||||
<term name="close-inner-quote">’</term>
|
||||
<term name="page-range-delimiter">–</term>
|
||||
<term name="colon">:</term>
|
||||
<term name="comma">,</term>
|
||||
<term name="semicolon">;</term>
|
||||
|
||||
<!-- ORDINALS -->
|
||||
<term name="ordinal">a</term>
|
||||
<term name="ordinal-01" gender-form="feminine" match="whole-number">a</term>
|
||||
<term name="ordinal-01" gender-form="masculine" match="whole-number">o</term>
|
||||
|
||||
<!-- LONG ORDINALS -->
|
||||
<term name="long-ordinal-01">primera</term>
|
||||
<term name="long-ordinal-02">segunda</term>
|
||||
<term name="long-ordinal-03">tercera</term>
|
||||
<term name="long-ordinal-04">cuarta</term>
|
||||
<term name="long-ordinal-05">quinta</term>
|
||||
<term name="long-ordinal-06">sexta</term>
|
||||
<term name="long-ordinal-07">séptima</term>
|
||||
<term name="long-ordinal-08">octava</term>
|
||||
<term name="long-ordinal-09">novena</term>
|
||||
<term name="long-ordinal-10">décima</term>
|
||||
|
||||
<!-- LONG LOCATOR FORMS -->
|
||||
<term name="act">
|
||||
<single>act</single>
|
||||
<multiple>acts</multiple>
|
||||
</term>
|
||||
<term name="appendix">
|
||||
<single>appendix</single>
|
||||
<multiple>appendices</multiple>
|
||||
</term>
|
||||
<term name="article-locator">
|
||||
<single>article</single>
|
||||
<multiple>articles</multiple>
|
||||
</term>
|
||||
<term name="book">
|
||||
<single>libro</single>
|
||||
<multiple>libros</multiple>
|
||||
</term>
|
||||
<term name="canon">
|
||||
<single>canon</single>
|
||||
<multiple>canons</multiple>
|
||||
</term>
|
||||
<term name="chapter">
|
||||
<single>capítulo</single>
|
||||
<multiple>capítulos</multiple>
|
||||
</term>
|
||||
<term name="column">
|
||||
<single>columna</single>
|
||||
<multiple>columnas</multiple>
|
||||
</term>
|
||||
<term name="elocation">
|
||||
<single>location</single>
|
||||
<multiple>locations</multiple>
|
||||
</term>
|
||||
<term name="equation">
|
||||
<single>equation</single>
|
||||
<multiple>equations</multiple>
|
||||
</term>
|
||||
<term name="figure">
|
||||
<single>figura</single>
|
||||
<multiple>figuras</multiple>
|
||||
</term>
|
||||
<term name="folio">
|
||||
<single>folio</single>
|
||||
<multiple>folios</multiple>
|
||||
</term>
|
||||
<term name="issue">
|
||||
<single>número</single>
|
||||
<multiple>números</multiple>
|
||||
</term>
|
||||
<term name="line">
|
||||
<single>línea</single>
|
||||
<multiple>líneas</multiple>
|
||||
</term>
|
||||
<term name="note">
|
||||
<single>nota</single>
|
||||
<multiple>notas</multiple>
|
||||
</term>
|
||||
<term name="opus">
|
||||
<single>opus</single>
|
||||
<multiple>opera</multiple>
|
||||
</term>
|
||||
<term name="page">
|
||||
<single>página</single>
|
||||
<multiple>páginas</multiple>
|
||||
</term>
|
||||
<term name="paragraph">
|
||||
<single>párrafo</single>
|
||||
<multiple>párrafos</multiple>
|
||||
</term>
|
||||
<term name="part">
|
||||
<single>parte</single>
|
||||
<multiple>partes</multiple>
|
||||
</term>
|
||||
<term name="rule">
|
||||
<single>rule</single>
|
||||
<multiple>rules</multiple>
|
||||
</term>
|
||||
<term name="scene">
|
||||
<single>scene</single>
|
||||
<multiple>scenes</multiple>
|
||||
</term>
|
||||
<term name="section">
|
||||
<single>sección</single>
|
||||
<multiple>secciones</multiple>
|
||||
</term>
|
||||
<term name="sub-verbo">
|
||||
<single>sub voce</single>
|
||||
<multiple>sub vocibus</multiple>
|
||||
</term>
|
||||
<term name="supplement">
|
||||
<single>supplement</single>
|
||||
<multiple>supplements</multiple>
|
||||
</term>
|
||||
<term name="table">
|
||||
<single>table</single>
|
||||
<multiple>tables</multiple>
|
||||
</term>
|
||||
<term name="timestamp"> <!-- generally blank -->
|
||||
<single/>
|
||||
<multiple/>
|
||||
</term>
|
||||
<term name="title-locator">
|
||||
<single>title</single>
|
||||
<multiple>titles</multiple>
|
||||
</term>
|
||||
<term name="verse">
|
||||
<single>verso</single>
|
||||
<multiple>versos</multiple>
|
||||
</term>
|
||||
<term name="volume">
|
||||
<single>volumen</single>
|
||||
<multiple>volúmenes</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SHORT LOCATOR FORMS -->
|
||||
<term name="appendix" form="short">
|
||||
<single>app.</single>
|
||||
<multiple>apps.</multiple>
|
||||
</term>
|
||||
<term name="article-locator" form="short">
|
||||
<single>art.</single>
|
||||
<multiple>arts.</multiple>
|
||||
</term>
|
||||
<term name="book" form="short">
|
||||
<single>lib.</single>
|
||||
<multiple>libs.</multiple>
|
||||
</term>
|
||||
<term name="chapter" form="short">
|
||||
<single>cap.</single>
|
||||
<multiple>caps.</multiple>
|
||||
</term>
|
||||
<term name="column" form="short">
|
||||
<single>col.</single>
|
||||
<multiple>cols.</multiple>
|
||||
</term>
|
||||
<term name="elocation" form="short">
|
||||
<single>loc.</single>
|
||||
<multiple>locs.</multiple>
|
||||
</term>
|
||||
<term name="equation" form="short">
|
||||
<single>eq.</single>
|
||||
<multiple>eqs.</multiple>
|
||||
</term>
|
||||
<term name="figure" form="short">
|
||||
<single>fig.</single>
|
||||
<multiple>figs.</multiple>
|
||||
</term>
|
||||
<term name="folio" form="short">
|
||||
<single>f.</single>
|
||||
<multiple>ff.</multiple>
|
||||
</term>
|
||||
<term name="issue" form="short">
|
||||
<single>núm.</single>
|
||||
<multiple>núms.</multiple>
|
||||
</term>
|
||||
<term name="line" form="short">
|
||||
<single>l.</single>
|
||||
<multiple>ls.</multiple>
|
||||
</term>
|
||||
<term name="note" form="short">
|
||||
<single>n.</single>
|
||||
<multiple>nn.</multiple>
|
||||
</term>
|
||||
<term name="opus" form="short">
|
||||
<single>op.</single>
|
||||
<multiple>opp.</multiple>
|
||||
</term>
|
||||
<term name="page" form="short">
|
||||
<single>p.</single>
|
||||
<multiple>pp.</multiple>
|
||||
</term>
|
||||
<term name="paragraph" form="short">
|
||||
<single>párr.</single>
|
||||
<multiple>párrs.</multiple>
|
||||
</term>
|
||||
<term name="part" form="short">
|
||||
<single>pt.</single>
|
||||
<multiple>pts.</multiple>
|
||||
</term>
|
||||
<term name="rule" form="short">
|
||||
<single>r.</single>
|
||||
<multiple>rr.</multiple>
|
||||
</term>
|
||||
<term name="scene" form="short">
|
||||
<single>sc.</single>
|
||||
<multiple>scs.</multiple>
|
||||
</term>
|
||||
<term name="section" form="short">
|
||||
<single>sec.</single>
|
||||
<multiple>secs.</multiple>
|
||||
</term>
|
||||
<term name="sub-verbo" form="short">
|
||||
<single>s. v.</single>
|
||||
<multiple>s. vv.</multiple>
|
||||
</term>
|
||||
<term name="supplement" form="short">
|
||||
<single>supp.</single>
|
||||
<multiple>supps.</multiple>
|
||||
</term>
|
||||
<term name="table" form="short">
|
||||
<single>tbl.</single>
|
||||
<multiple>tbls.</multiple>
|
||||
</term>
|
||||
<term name="timestamp" form="short"> <!-- generally blank -->
|
||||
<single/>
|
||||
<multiple/>
|
||||
</term>
|
||||
<term name="title-locator" form="short">
|
||||
<single>tit.</single>
|
||||
<multiple>tits.</multiple>
|
||||
</term>
|
||||
<term name="verse" form="short">
|
||||
<single>v.</single>
|
||||
<multiple>vv.</multiple>
|
||||
</term>
|
||||
<term name="volume" form="short">
|
||||
<single>vol.</single>
|
||||
<multiple>vols.</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SYMBOLIC LOCATOR FORMS -->
|
||||
<term name="paragraph" form="symbol">
|
||||
<single>¶</single>
|
||||
<multiple>¶</multiple>
|
||||
</term>
|
||||
<term name="section" form="symbol">
|
||||
<single>§</single>
|
||||
<multiple>§</multiple>
|
||||
</term>
|
||||
|
||||
<!-- LONG NUMBER VARIABLE FORMS -->
|
||||
<term name="chapter-number">
|
||||
<single>chapter</single>
|
||||
<multiple>chapters</multiple>
|
||||
</term>
|
||||
<term name="citation-number">
|
||||
<single>citation</single>
|
||||
<multiple>citations</multiple>
|
||||
</term>
|
||||
<term name="collection-number">
|
||||
<single>número</single>
|
||||
<multiple>números</multiple>
|
||||
</term>
|
||||
<term name="edition">
|
||||
<single>edición</single>
|
||||
<multiple>ediciones</multiple>
|
||||
</term>
|
||||
<term name="first-reference-note-number">
|
||||
<single>reference</single>
|
||||
<multiple>references</multiple>
|
||||
</term>
|
||||
<term name="number">
|
||||
<single>number</single>
|
||||
<multiple>numbers</multiple>
|
||||
</term>
|
||||
<term name="number-of-pages">
|
||||
<single>página</single>
|
||||
<multiple>páginas</multiple>
|
||||
</term>
|
||||
<term name="number-of-volumes">
|
||||
<single>volume</single>
|
||||
<multiple>volumes</multiple>
|
||||
</term>
|
||||
<term name="page-first">
|
||||
<single>page</single>
|
||||
<multiple>pages</multiple>
|
||||
</term>
|
||||
<term name="printing">
|
||||
<single>printing</single>
|
||||
<multiple>printings</multiple>
|
||||
</term>
|
||||
<term name="version">versión</term>
|
||||
|
||||
<!-- SHORT NUMBER VARIABLE FORMS -->
|
||||
<term name="chapter-number" form="short">
|
||||
<single>chap.</single>
|
||||
<multiple>chaps.</multiple>
|
||||
</term>
|
||||
<term name="citation-number" form="short">
|
||||
<single>cit.</single>
|
||||
<multiple>cits.</multiple>
|
||||
</term>
|
||||
<term name="collection-number" form="short">
|
||||
<single>núm.</single>
|
||||
<multiple>núms.</multiple>
|
||||
</term>
|
||||
<term name="edition" form="short">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="first-reference-note-number" form="short">
|
||||
<single>ref.</single>
|
||||
<multiple>refs.</multiple>
|
||||
</term>
|
||||
<term name="number" form="short">
|
||||
<single>no.</single>
|
||||
<multiple>nos.</multiple>
|
||||
</term>
|
||||
<term name="number-of-pages" form="short">
|
||||
<single>p.</single>
|
||||
<multiple>pp.</multiple>
|
||||
</term>
|
||||
<term name="number-of-volumes" form="short">
|
||||
<single>vol.</single>
|
||||
<multiple>vols.</multiple>
|
||||
</term>
|
||||
<term name="page-first" form="short">
|
||||
<single>p.</single>
|
||||
<multiple>pp.</multiple>
|
||||
</term>
|
||||
<term name="printing" form="short">
|
||||
<single>print.</single>
|
||||
<multiple>prints.</multiple>
|
||||
</term>
|
||||
|
||||
<!-- LONG ROLE FORMS -->
|
||||
<term name="author"/> <!-- generally blank -->
|
||||
<term name="chair">
|
||||
<single>chair</single>
|
||||
<multiple>chairs</multiple>
|
||||
</term>
|
||||
<term name="collection-editor">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="compiler">
|
||||
<single>compiler</single>
|
||||
<multiple>compilers</multiple>
|
||||
</term>
|
||||
<term name="composer"/> <!-- generally blank -->
|
||||
<term name="container-author"/> <!-- generally blank -->
|
||||
<term name="contributor">
|
||||
<single>contributor</single>
|
||||
<multiple>contributors</multiple>
|
||||
</term>
|
||||
<term name="curator">
|
||||
<single>curator</single>
|
||||
<multiple>curators</multiple>
|
||||
</term>
|
||||
<term name="director">
|
||||
<single>director</single>
|
||||
<multiple>directores</multiple>
|
||||
</term>
|
||||
<term name="editor">
|
||||
<single>editor</single>
|
||||
<multiple>editores</multiple>
|
||||
</term>
|
||||
<term name="editor-translator">
|
||||
<single>editor y traductor</single>
|
||||
<multiple>editores y traductores</multiple>
|
||||
</term>
|
||||
<term name="editortranslator">
|
||||
<single>editor y traductor</single>
|
||||
<multiple>editores y traductores</multiple>
|
||||
</term>
|
||||
<term name="editorial-director">
|
||||
<single>coordinador</single>
|
||||
<multiple>coordinadores</multiple>
|
||||
</term>
|
||||
<term name="executive-producer">
|
||||
<single>executive producer</single>
|
||||
<multiple>executive producers</multiple>
|
||||
</term>
|
||||
<term name="guest">
|
||||
<single>guest</single>
|
||||
<multiple>guests</multiple>
|
||||
</term>
|
||||
<term name="host">
|
||||
<single>host</single>
|
||||
<multiple>hosts</multiple>
|
||||
</term>
|
||||
<term name="illustrator">
|
||||
<single>ilustrador</single>
|
||||
<multiple>ilustradores</multiple>
|
||||
</term>
|
||||
<term name="interviewer"/> <!-- generally blank -->
|
||||
<term name="narrator">
|
||||
<single>narrator</single>
|
||||
<multiple>narrators</multiple>
|
||||
</term>
|
||||
<term name="organizer">
|
||||
<single>organizer</single>
|
||||
<multiple>organizers</multiple>
|
||||
</term>
|
||||
<term name="original-author"/> <!-- generally blank -->
|
||||
<term name="performer">
|
||||
<single>performer</single>
|
||||
<multiple>performers</multiple>
|
||||
</term>
|
||||
<term name="producer">
|
||||
<single>producer</single>
|
||||
<multiple>producers</multiple>
|
||||
</term>
|
||||
<term name="recipient"/> <!-- generally blank -->
|
||||
<term name="reviewed-author"/> <!-- generally blank -->
|
||||
<term name="script-writer">
|
||||
<single>writer</single>
|
||||
<multiple>writers</multiple>
|
||||
</term>
|
||||
<term name="series-creator">
|
||||
<single>series creator</single>
|
||||
<multiple>series creators</multiple>
|
||||
</term>
|
||||
<term name="translator">
|
||||
<single>traductor</single>
|
||||
<multiple>traductores</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SHORT ROLE FORMS -->
|
||||
<term name="compiler" form="short">
|
||||
<single>comp.</single>
|
||||
<multiple>comps.</multiple>
|
||||
</term>
|
||||
<term name="contributor" form="short">
|
||||
<single>contrib.</single>
|
||||
<multiple>contribs.</multiple>
|
||||
</term>
|
||||
<term name="curator" form="short">
|
||||
<single>cur.</single>
|
||||
<multiple>curs.</multiple>
|
||||
</term>
|
||||
<term name="director" form="short">
|
||||
<single>dir.</single>
|
||||
<multiple>dirs.</multiple>
|
||||
</term>
|
||||
<term name="editor" form="short">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="editor-translator" form="short">
|
||||
<single>ed. y trad.</single>
|
||||
<multiple>eds. y trads.</multiple>
|
||||
</term>
|
||||
<term name="editortranslator" form="short">
|
||||
<single>ed. y trad.</single>
|
||||
<multiple>eds. y trads.</multiple>
|
||||
</term>
|
||||
<term name="editorial-director" form="short">
|
||||
<single>coord.</single>
|
||||
<multiple>coords.</multiple>
|
||||
</term>
|
||||
<term name="executive-producer" form="short">
|
||||
<single>exec. prod.</single>
|
||||
<multiple>exec. prods.</multiple>
|
||||
</term>
|
||||
<term name="illustrator" form="short">
|
||||
<single>ilust.</single>
|
||||
<multiple>ilusts.</multiple>
|
||||
</term>
|
||||
<term name="narrator" form="short">
|
||||
<single>narr.</single>
|
||||
<multiple>narrs.</multiple>
|
||||
</term>
|
||||
<term name="organizer" form="short">
|
||||
<single>org.</single>
|
||||
<multiple>orgs.</multiple>
|
||||
</term>
|
||||
<term name="performer" form="short">
|
||||
<single>perf.</single>
|
||||
<multiple>perfs.</multiple>
|
||||
</term>
|
||||
<term name="producer" form="short">
|
||||
<single>prod.</single>
|
||||
<multiple>prods.</multiple>
|
||||
</term>
|
||||
<term name="script-writer" form="short">
|
||||
<single>writ.</single>
|
||||
<multiple>writs.</multiple>
|
||||
</term>
|
||||
<term name="series-creator" form="short">
|
||||
<single>cre.</single>
|
||||
<multiple>cres.</multiple>
|
||||
</term>
|
||||
<term name="translator" form="short">
|
||||
<single>trad.</single>
|
||||
<multiple>trads.</multiple>
|
||||
</term>
|
||||
|
||||
<!-- VERB ROLE FORMS -->
|
||||
<term name="chair" form="verb">chaired by</term>
|
||||
<term name="collection-editor" form="verb">edited by</term>
|
||||
<term name="compiler" form="verb">compiled by</term>
|
||||
<term name="container-author" form="verb">de</term>
|
||||
<term name="contributor" form="verb">with</term>
|
||||
<term name="curator" form="verb">curated by</term>
|
||||
<term name="director" form="verb">dirigido por</term>
|
||||
<term name="editor" form="verb">editado por</term>
|
||||
<term name="editor-translator" form="verb">editado y traducido por</term>
|
||||
<term name="editortranslator" form="verb">editado y traducido por</term>
|
||||
<term name="editorial-director" form="verb">coordinado por</term>
|
||||
<term name="executive-producer" form="verb">executive produced by</term>
|
||||
<term name="guest" form="verb">with guest</term>
|
||||
<term name="host" form="verb">hosted by</term>
|
||||
<term name="illustrator" form="verb">ilustrado por</term>
|
||||
<term name="interviewer" form="verb">entrevistado por</term>
|
||||
<term name="narrator" form="verb">narrated by</term>
|
||||
<term name="organizer" form="verb">organized by</term>
|
||||
<term name="performer" form="verb">performed by</term>
|
||||
<term name="producer" form="verb">produced by</term>
|
||||
<term name="recipient" form="verb">a</term>
|
||||
<term name="reviewed-author" form="verb">por</term>
|
||||
<term name="script-writer" form="verb">written by</term>
|
||||
<term name="series-creator" form="verb">created by</term>
|
||||
<term name="translator" form="verb">traducido por</term>
|
||||
|
||||
<!-- SHORT VERB ROLE FORMS -->
|
||||
<term name="collection-editor" form="verb-short">ed. by</term>
|
||||
<term name="compiler" form="verb-short">comp. by</term>
|
||||
<term name="contributor" form="verb-short">w.</term>
|
||||
<term name="curator" form="verb-short">cur. by</term>
|
||||
<term name="director" form="verb-short">dir.</term>
|
||||
<term name="editor" form="verb-short">ed.</term>
|
||||
<term name="editor-translator" form="verb-short">ed. y trad.</term>
|
||||
<term name="editortranslator" form="verb-short">ed. y trad.</term>
|
||||
<term name="editorial-director" form="verb-short">coord.</term>
|
||||
<term name="executive-producer" form="verb-short">exec. prod. by</term>
|
||||
<term name="guest" form="verb-short">w. guest</term>
|
||||
<term name="host" form="verb-short">hosted by</term>
|
||||
<term name="illustrator" form="verb-short">ilust.</term>
|
||||
<term name="narrator" form="verb-short">narr. by</term>
|
||||
<term name="organizer" form="verb-short">org. by</term>
|
||||
<term name="performer" form="verb-short">perf. by</term>
|
||||
<term name="producer" form="verb-short">prod. by</term>
|
||||
<term name="script-writer" form="verb-short">writ. by</term>
|
||||
<term name="series-creator" form="verb-short">cre. by</term>
|
||||
<term name="translator" form="verb-short">trad.</term>
|
||||
|
||||
<!-- LONG MONTH FORMS -->
|
||||
<term name="month-01">enero</term>
|
||||
<term name="month-02">febrero</term>
|
||||
<term name="month-03">marzo</term>
|
||||
<term name="month-04">abril</term>
|
||||
<term name="month-05">mayo</term>
|
||||
<term name="month-06">junio</term>
|
||||
<term name="month-07">julio</term>
|
||||
<term name="month-08">agosto</term>
|
||||
<term name="month-09">septiembre</term>
|
||||
<term name="month-10">octubre</term>
|
||||
<term name="month-11">noviembre</term>
|
||||
<term name="month-12">diciembre</term>
|
||||
|
||||
<!-- SHORT MONTH FORMS -->
|
||||
<term name="month-01" form="short">ene.</term>
|
||||
<term name="month-02" form="short">feb.</term>
|
||||
<term name="month-03" form="short">mar.</term>
|
||||
<term name="month-04" form="short">abr.</term>
|
||||
<term name="month-05" form="short">may</term>
|
||||
<term name="month-06" form="short">jun.</term>
|
||||
<term name="month-07" form="short">jul.</term>
|
||||
<term name="month-08" form="short">ago.</term>
|
||||
<term name="month-09" form="short">sep.</term>
|
||||
<term name="month-10" form="short">oct.</term>
|
||||
<term name="month-11" form="short">nov.</term>
|
||||
<term name="month-12" form="short">dic.</term>
|
||||
|
||||
<!-- SEASONS -->
|
||||
<term name="season-01">primavera</term>
|
||||
<term name="season-02">verano</term>
|
||||
<term name="season-03">otoño</term>
|
||||
<term name="season-04">invierno</term>
|
||||
</terms>
|
||||
</locale>
|
||||
@@ -0,0 +1,520 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" delimiter-precedes-last="always" demote-non-dropping-particle="sort-only" initialize-with="" initialize-with-hyphen="false" name-as-sort-order="all" name-delimiter=", " names-delimiter=", " page-range-format="minimal" sort-separator=" " version="1.0">
|
||||
<!-- This file was generated by the Style Variant Builder <https://github.com/citation-style-language/style-variant-builder>. To contribute changes, modify the template and regenerate variants. -->
|
||||
<info>
|
||||
<title>NLM/Vancouver: Citing Medicine 2nd edition (citation-sequence)</title>
|
||||
<title-short>National Library of Medicine, ANSI/NISO Z39.29-2005 (R2010), ICMJE Recommendations/URMs (C-S)</title-short>
|
||||
<id>http://www.zotero.org/styles/nlm-citation-sequence</id>
|
||||
<link href="http://www.zotero.org/styles/nlm-citation-sequence" rel="self"/>
|
||||
<link href="https://www.nlm.nih.gov/citingmedicine" rel="documentation"/>
|
||||
<link href="https://www.nlm.nih.gov/bsd/uniform_requirements.html" rel="documentation"/>
|
||||
<link href="https://www.icmje.org/recommendations/" rel="documentation"/>
|
||||
<author>
|
||||
<name>Michael Berkowitz</name>
|
||||
<email>mberkowi@gmu.edu</email>
|
||||
</author>
|
||||
<author>
|
||||
<name>Andrew Dunning</name>
|
||||
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
||||
</author>
|
||||
<contributor>
|
||||
<name>Petr Hlustik</name>
|
||||
<uri>https://orcid.org/0000-0002-1951-0671</uri>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Sebastian Karcher</name>
|
||||
<uri>https://orcid.org/0000-0001-8249-7388</uri>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Charles Parnot</name>
|
||||
<uri>https://orcid.org/0000-0002-7346-5883</uri>
|
||||
</contributor>
|
||||
<contributor>
|
||||
<name>Sean Takats</name>
|
||||
<uri>https://orcid.org/0000-0002-7851-5069</uri>
|
||||
</contributor>
|
||||
<category citation-format="numeric"/>
|
||||
<category field="generic-base"/>
|
||||
<category field="medicine"/>
|
||||
<category field="science"/>
|
||||
<summary>Citing Medicine: The NLM Style Guide for Authors, Editors, and Publishers, 2nd edition (2015), based on ANSI/NISO Z39.29-2005 (R2010); citation-sequence system.</summary>
|
||||
<updated>2026-02-18T15:24:08+00:00</updated>
|
||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||
</info>
|
||||
<locale xml:lang="en">
|
||||
<date delimiter=" " form="text">
|
||||
<date-part name="year"/>
|
||||
<date-part form="short" name="month" strip-periods="true"/>
|
||||
<date-part name="day"/>
|
||||
</date>
|
||||
<terms>
|
||||
<term name="available at">available from</term>
|
||||
<term name="collection-editor">
|
||||
<single>editor</single>
|
||||
<multiple>editors</multiple>
|
||||
</term>
|
||||
<term form="short" name="month-06">Jun.</term>
|
||||
<term form="short" name="month-07">Jul.</term>
|
||||
<term form="short" name="month-09">Sep.</term>
|
||||
<term name="presented at">presented at</term>
|
||||
<term form="short" name="section">
|
||||
<single>sect.</single>
|
||||
<multiple>sects.</multiple>
|
||||
</term>
|
||||
<term form="short" name="supplement">
|
||||
<single>suppl.</single>
|
||||
<multiple>suppls.</multiple>
|
||||
</term>
|
||||
</terms>
|
||||
</locale>
|
||||
<locale xml:lang="fr">
|
||||
<date delimiter=" " form="text">
|
||||
<date-part name="day"/>
|
||||
<date-part form="short" name="month" strip-periods="true"/>
|
||||
<date-part name="year"/>
|
||||
</date>
|
||||
</locale>
|
||||
<!-- Variable labels -->
|
||||
<macro name="label-collection-number">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if is-numeric="collection-number">
|
||||
<label form="short" variable="collection-number"/>
|
||||
</if>
|
||||
</choose>
|
||||
<text variable="collection-number"/>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="label-edition">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if is-numeric="edition">
|
||||
<number form="ordinal" variable="edition"/>
|
||||
<label form="short" variable="edition"/>
|
||||
</if>
|
||||
<else>
|
||||
<text variable="edition"/>
|
||||
</else>
|
||||
</choose>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="label-number-of-pages">
|
||||
<group delimiter=" ">
|
||||
<text variable="number-of-pages"/>
|
||||
<choose>
|
||||
<if is-numeric="number-of-pages">
|
||||
<label form="short" plural="never" variable="number-of-pages"/>
|
||||
</if>
|
||||
</choose>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="label-page">
|
||||
<group delimiter=" ">
|
||||
<label form="short" plural="never" variable="page"/>
|
||||
<text variable="page"/>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="label-part-number-capitalized">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if is-numeric="part-number">
|
||||
<!-- TODO: Replace with `part-number` label when CSL provides one -->
|
||||
<text form="short" term="part" text-case="capitalize-first"/>
|
||||
</if>
|
||||
</choose>
|
||||
<text variable="part-number"/>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="label-supplement-number">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if is-numeric="supplement-number">
|
||||
<!-- TODO: Replace with `supplement-number` label when CSL provides one -->
|
||||
<text form="short" strip-periods="true" term="supplement" text-case="capitalize-first"/>
|
||||
</if>
|
||||
</choose>
|
||||
<text text-case="capitalize-first" variable="supplement-number"/>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="label-volume-capitalized">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if is-numeric="volume">
|
||||
<label form="short" text-case="capitalize-first" variable="volume"/>
|
||||
</if>
|
||||
</choose>
|
||||
<text variable="volume"/>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="author">
|
||||
<names variable="author">
|
||||
<label prefix=", "/>
|
||||
<substitute>
|
||||
<names variable="editor-translator"/>
|
||||
<names variable="editor translator"/>
|
||||
<names variable="editor"/>
|
||||
<names variable="collection-editor"/>
|
||||
</substitute>
|
||||
</names>
|
||||
</macro>
|
||||
<macro name="title">
|
||||
<choose>
|
||||
<if type="webpage" variable="container-title">
|
||||
<!-- `webpage` listed under `container-title` (Citing Medicine, ch. 25) -->
|
||||
<text variable="container-title"/>
|
||||
</if>
|
||||
<else>
|
||||
<text variable="title"/>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="content-type">
|
||||
<text variable="genre"/>
|
||||
</macro>
|
||||
<macro name="type-of-medium">
|
||||
<choose>
|
||||
<if variable="medium">
|
||||
<text text-case="capitalize-first" variable="medium"/>
|
||||
</if>
|
||||
<else-if match="any" type="chapter entry-dictionary entry-encyclopedia paper-conference"/>
|
||||
<else-if variable="URL">
|
||||
<text term="internet" text-case="capitalize-first"/>
|
||||
</else-if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="container-preposition">
|
||||
<choose>
|
||||
<if match="any" type="chapter paper-conference entry-dictionary entry-encyclopedia">
|
||||
<text term="in" text-case="capitalize-first"/>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="secondary-authors">
|
||||
<names variable="editor">
|
||||
<label prefix=", "/>
|
||||
</names>
|
||||
</macro>
|
||||
<macro name="container-title">
|
||||
<group delimiter=", ">
|
||||
<choose>
|
||||
<if type="webpage"/>
|
||||
<else-if variable="container-title">
|
||||
<group delimiter=". ">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if match="any" type="article-journal review review-book">
|
||||
<text form="short" strip-periods="true" variable="container-title"/>
|
||||
</if>
|
||||
<else>
|
||||
<text variable="container-title"/>
|
||||
</else>
|
||||
</choose>
|
||||
<choose>
|
||||
<if type="article-journal" variable="DOI"/>
|
||||
<else-if type="article-journal" variable="PMID"/>
|
||||
<else-if type="article-journal" variable="PMCID"/>
|
||||
<else-if variable="URL">
|
||||
<text prefix="[" suffix="]" term="internet" text-case="capitalize-first"/>
|
||||
</else-if>
|
||||
</choose>
|
||||
</group>
|
||||
<text macro="label-edition"/>
|
||||
</group>
|
||||
</else-if>
|
||||
<!-- TODO: add `event-name` and `event-place` -->
|
||||
<else-if match="any" type="bill legislation">
|
||||
<group delimiter=". ">
|
||||
<text variable="container-title"/>
|
||||
<group delimiter=" ">
|
||||
<text form="short" term="section" text-case="capitalize-first"/>
|
||||
<text variable="section"/>
|
||||
</group>
|
||||
</group>
|
||||
<text variable="number"/>
|
||||
</else-if>
|
||||
<else-if type="speech">
|
||||
<group delimiter=": ">
|
||||
<group delimiter=" ">
|
||||
<text text-case="capitalize-first" variable="genre"/>
|
||||
<text term="presented at"/>
|
||||
</group>
|
||||
<text variable="event"/>
|
||||
</group>
|
||||
</else-if>
|
||||
<else>
|
||||
<group delimiter=", ">
|
||||
<text macro="label-volume-capitalized"/>
|
||||
<text variable="volume-title"/>
|
||||
</group>
|
||||
<group delimiter=", ">
|
||||
<text macro="label-part-number-capitalized"/>
|
||||
<text variable="part-title"/>
|
||||
</group>
|
||||
</else>
|
||||
</choose>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="place-of-publication">
|
||||
<choose>
|
||||
<if type="thesis">
|
||||
<text prefix="[" suffix="]" variable="publisher-place"/>
|
||||
</if>
|
||||
<else-if type="speech"/>
|
||||
<else>
|
||||
<text variable="publisher-place"/>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="publisher">
|
||||
<choose>
|
||||
<!-- discard publisher for serial publications -->
|
||||
<if match="none" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
|
||||
<group delimiter=": ">
|
||||
<text macro="place-of-publication"/>
|
||||
<text variable="publisher"/>
|
||||
</group>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="date">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
|
||||
<group delimiter=":">
|
||||
<group delimiter=" ">
|
||||
<date form="text" variable="issued"/>
|
||||
<choose>
|
||||
<if type="article-journal" variable="DOI"/>
|
||||
<else-if type="article-journal" variable="PMID"/>
|
||||
<else-if type="article-journal" variable="PMCID"/>
|
||||
<else>
|
||||
<text macro="date-of-citation"/>
|
||||
</else>
|
||||
</choose>
|
||||
</group>
|
||||
<choose>
|
||||
<if type="article-newspaper">
|
||||
<text variable="page"/>
|
||||
</if>
|
||||
</choose>
|
||||
</group>
|
||||
</if>
|
||||
<else-if match="any" type="bill legislation">
|
||||
<date form="text" variable="issued"/>
|
||||
</else-if>
|
||||
<else-if type="report">
|
||||
<date date-parts="year-month" form="text" variable="issued"/>
|
||||
<text macro="date-of-citation"/>
|
||||
</else-if>
|
||||
<else-if type="patent">
|
||||
<group delimiter=", ">
|
||||
<text variable="number"/>
|
||||
<date date-parts="year" form="numeric" variable="issued"/>
|
||||
</group>
|
||||
<text macro="date-of-citation"/>
|
||||
</else-if>
|
||||
<else-if type="speech">
|
||||
<group delimiter="; ">
|
||||
<group delimiter=" ">
|
||||
<date form="text" variable="issued"/>
|
||||
<text macro="date-of-citation"/>
|
||||
</group>
|
||||
<text variable="event-place"/>
|
||||
</group>
|
||||
</else-if>
|
||||
<else>
|
||||
<date date-parts="year" form="numeric" variable="issued"/>
|
||||
<text macro="date-of-citation"/>
|
||||
</else>
|
||||
</choose>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="identifier-serial">
|
||||
<choose>
|
||||
<if match="any" type="article-journal article-magazine periodical post-weblog review review-book">
|
||||
<group delimiter=":">
|
||||
<group>
|
||||
<text variable="collection-title"/>
|
||||
<text variable="volume"/>
|
||||
<group delimiter=" " prefix="(" suffix=")">
|
||||
<text variable="issue"/>
|
||||
<text macro="label-supplement-number"/>
|
||||
</group>
|
||||
</group>
|
||||
<text macro="location-pagination-serial"/>
|
||||
</group>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="date-of-citation">
|
||||
<choose>
|
||||
<if variable="URL">
|
||||
<group delimiter=" " prefix="[" suffix="]">
|
||||
<text term="cited"/>
|
||||
<date form="text" variable="accessed"/>
|
||||
</group>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="location-pagination-monographic">
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if match="any" type="article-journal article-magazine article-newspaper review review-book"/>
|
||||
<else-if type="book">
|
||||
<text macro="label-number-of-pages"/>
|
||||
</else-if>
|
||||
<else>
|
||||
<text macro="label-page"/>
|
||||
</else>
|
||||
</choose>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="location-pagination-serial">
|
||||
<choose>
|
||||
<if variable="number">
|
||||
<text variable="number"/>
|
||||
</if>
|
||||
<else>
|
||||
<text variable="page"/>
|
||||
</else>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="webpage-part">
|
||||
<choose>
|
||||
<if type="webpage" variable="container-title">
|
||||
<text variable="title"/>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="series">
|
||||
<choose>
|
||||
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book"/>
|
||||
<else-if variable="collection-title">
|
||||
<group delimiter=". " prefix="(" suffix=")">
|
||||
<names variable="collection-editor">
|
||||
<label prefix=", "/>
|
||||
</names>
|
||||
<group delimiter="; ">
|
||||
<text variable="collection-title"/>
|
||||
<text macro="label-collection-number"/>
|
||||
</group>
|
||||
</group>
|
||||
</else-if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="report-number">
|
||||
<choose>
|
||||
<if type="report">
|
||||
<group delimiter=": ">
|
||||
<group delimiter=" ">
|
||||
<text term="report" text-case="capitalize-first"/>
|
||||
<label form="short" text-case="capitalize-first" variable="number"/>
|
||||
</group>
|
||||
<text variable="number"/>
|
||||
</group>
|
||||
</if>
|
||||
</choose>
|
||||
</macro>
|
||||
<macro name="availability">
|
||||
<group delimiter=". ">
|
||||
<group delimiter=": ">
|
||||
<text text-case="capitalize-first" value="located at"/>
|
||||
<group delimiter="; ">
|
||||
<group delimiter=", ">
|
||||
<text variable="archive_collection"/>
|
||||
<text variable="archive"/>
|
||||
<text variable="archive-place"/>
|
||||
</group>
|
||||
<text variable="archive_location"/>
|
||||
</group>
|
||||
</group>
|
||||
<group delimiter=" ">
|
||||
<choose>
|
||||
<if type="article-journal" variable="DOI"/>
|
||||
<else-if type="article-journal" variable="PMID"/>
|
||||
<else-if type="article-journal" variable="PMCID"/>
|
||||
<else>
|
||||
<group delimiter=": ">
|
||||
<text term="available at" text-case="capitalize-first"/>
|
||||
<text variable="URL"/>
|
||||
</group>
|
||||
</else>
|
||||
</choose>
|
||||
<text prefix="doi:" variable="DOI"/>
|
||||
</group>
|
||||
</group>
|
||||
</macro>
|
||||
<macro name="notes">
|
||||
<group delimiter=". " suffix=".">
|
||||
<group delimiter="; ">
|
||||
<group delimiter=": ">
|
||||
<text value="PubMed PMID"/>
|
||||
<text variable="PMID"/>
|
||||
</group>
|
||||
<group delimiter=": ">
|
||||
<text value="PubMed Central PMCID"/>
|
||||
<text variable="PMCID"/>
|
||||
</group>
|
||||
</group>
|
||||
<text variable="references"/>
|
||||
</group>
|
||||
</macro>
|
||||
<citation collapse="citation-number">
|
||||
<sort>
|
||||
<key variable="citation-number"/>
|
||||
</sort>
|
||||
<layout delimiter="," prefix="(" suffix=")">
|
||||
<text variable="citation-number"/>
|
||||
</layout>
|
||||
</citation>
|
||||
<macro name="bibliography">
|
||||
<group delimiter=" ">
|
||||
<group delimiter=". " suffix=".">
|
||||
<text macro="author"/>
|
||||
<group delimiter=" ">
|
||||
<text macro="title"/>
|
||||
<text macro="content-type" prefix="[" suffix="]"/>
|
||||
<choose>
|
||||
<if type="webpage" variable="container-title">
|
||||
<text macro="type-of-medium" prefix="[" suffix="]"/>
|
||||
</if>
|
||||
<else-if match="none" variable="container-title">
|
||||
<text macro="type-of-medium" prefix="[" suffix="]"/>
|
||||
</else-if>
|
||||
</choose>
|
||||
</group>
|
||||
<choose>
|
||||
<if match="none" variable="container-title">
|
||||
<text macro="label-edition"/>
|
||||
</if>
|
||||
</choose>
|
||||
<group delimiter=": ">
|
||||
<text macro="container-preposition"/>
|
||||
<group delimiter=". ">
|
||||
<text macro="secondary-authors"/>
|
||||
<text macro="container-title"/>
|
||||
</group>
|
||||
</group>
|
||||
<group delimiter="; ">
|
||||
<text macro="publisher"/>
|
||||
<group delimiter=";">
|
||||
<text macro="date"/>
|
||||
<text macro="identifier-serial"/>
|
||||
</group>
|
||||
</group>
|
||||
<text macro="location-pagination-monographic"/>
|
||||
<text macro="webpage-part"/>
|
||||
<text macro="series"/>
|
||||
<text macro="report-number"/>
|
||||
</group>
|
||||
<text macro="availability"/>
|
||||
<text macro="notes"/>
|
||||
</group>
|
||||
</macro>
|
||||
<bibliography et-al-min="7" et-al-use-first="6" second-field-align="flush">
|
||||
<layout>
|
||||
<text suffix="." variable="citation-number"/>
|
||||
<text macro="bibliography"/>
|
||||
</layout>
|
||||
</bibliography>
|
||||
</style>
|
||||
@@ -0,0 +1,354 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3b0acbc3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import { createClient } from \"@supabase/supabase-js\";\n",
|
||||
"const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n",
|
||||
"const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n",
|
||||
"\n",
|
||||
"const supabase = createClient(supabaseUrl, supabaseKey);\n",
|
||||
"const supa = await supabase.auth.signInWithPassword({\n",
|
||||
" email: 'guillermo.arrieta@lasalle.mx',\n",
|
||||
" password: 'admin',\n",
|
||||
"});"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "be47eac0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6ef7e79f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{\n",
|
||||
" access_token: \"eyJhbGciOiJFUzI1NiIsImtpZCI6ImI4MTI2OWYxLTIxZDgtNGYyZS1iNzE5LWMyMjQwYTg0MGQ5MCIsInR5cCI6IkpXVCJ9.eyJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MjQ3MzI3MX1dLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwiYXVkIjoiYXV0aGVudGljYXRlZCIsImVtYWlsIjoiZ3VpbGxlcm1vLmFycmlldGFAbGFzYWxsZS5teCIsImV4cCI6MTc3MjQ3Njg3MSwiZmVhdHVyZV94Ijp0cnVlLCJpYXQiOjE3NzI0NzMyNzEsImlzX2Fub255bW91cyI6ZmFsc2UsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTQzMjEvYXV0aC92MSIsInBob25lIjoiIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJzZXNzaW9uX2lkIjoiZjc2ZjRkZGYtZTcxOS00NjRiLWIwYWUtODBhNmIxMjBlZmE0Iiwic3ViIjoiOTVkMWYzODEtNzAyMy00ZjMzLWJjNTQtMDhjY2QwOGFiZjQxIiwidGVuYW50X2lkIjoibG9jYWwtZGV2LXRlbmFudCIsInVzZXJfbWV0YWRhdGEiOnsiZW1haWxfdmVyaWZpZWQiOnRydWV9fQ.LEXrbBNetdCb8zfCiLcXhJg3EmX6XuE4cYOaFmYg1PhvvNI5XorbJQHhb4SQVggQDOauIqqkMRsK64GK1nheIw\",\n",
|
||||
" token_type: \"bearer\",\n",
|
||||
" expires_in: 3600,\n",
|
||||
" expires_at: 1772476871,\n",
|
||||
" refresh_token: \"lwgtrzw2tpzl\",\n",
|
||||
" user: {\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" aud: \"authenticated\",\n",
|
||||
" role: \"authenticated\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" phone: \"\",\n",
|
||||
" confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" last_sign_in_at: \"2026-03-02T17:41:10.98658403Z\",\n",
|
||||
" app_metadata: { provider: \"email\", providers: [ \"email\" ] },\n",
|
||||
" user_metadata: { email_verified: true },\n",
|
||||
" identities: [\n",
|
||||
" {\n",
|
||||
" identity_id: \"b2f3094d-a29b-42b5-be69-3397282ffcd7\",\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" user_id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" identity_data: {\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_verified: false,\n",
|
||||
" phone_verified: false,\n",
|
||||
" sub: \"95d1f381-7023-4f33-bc54-08ccd08abf41\"\n",
|
||||
" },\n",
|
||||
" provider: \"email\",\n",
|
||||
" last_sign_in_at: \"2026-01-13T14:54:39.085382Z\",\n",
|
||||
" created_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" updated_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\"\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" created_at: \"2026-01-13T14:54:39.081453Z\",\n",
|
||||
" updated_at: \"2026-03-02T17:41:11.002577Z\",\n",
|
||||
" is_anonymous: false\n",
|
||||
" },\n",
|
||||
" weak_password: {\n",
|
||||
" message: \"Password should be at least 6 characters.\",\n",
|
||||
" reasons: [ \"length\" ]\n",
|
||||
" }\n",
|
||||
"}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{\n",
|
||||
" data: {\n",
|
||||
" subscription: {\n",
|
||||
" id: \u001b[32mSymbol(\"auth-callback\")\u001b[39m,\n",
|
||||
" callback: \u001b[36m[AsyncFunction (anonymous)]\u001b[39m,\n",
|
||||
" unsubscribe: \u001b[36m[Function: unsubscribe]\u001b[39m\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"TOKEN_REFRESHED {\n",
|
||||
" aal: \"aal1\",\n",
|
||||
" amr: [ { method: \"password\", timestamp: 1772473271 } ],\n",
|
||||
" app_metadata: { provider: \"email\", providers: [ \"email\" ] },\n",
|
||||
" aud: \"authenticated\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" exp: 1772480381,\n",
|
||||
" feature_x: true,\n",
|
||||
" iat: 1772476781,\n",
|
||||
" is_anonymous: false,\n",
|
||||
" iss: \"http://127.0.0.1:54321/auth/v1\",\n",
|
||||
" phone: \"\",\n",
|
||||
" role: \"authenticated\",\n",
|
||||
" session_id: \"f76f4ddf-e719-464b-b0ae-80a6b120efa4\",\n",
|
||||
" sub: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" tenant_id: \"local-dev-tenant\",\n",
|
||||
" user_metadata: { email_verified: true }\n",
|
||||
"}\n",
|
||||
"{\n",
|
||||
" access_token: \"eyJhbGciOiJFUzI1NiIsImtpZCI6ImI4MTI2OWYxLTIxZDgtNGYyZS1iNzE5LWMyMjQwYTg0MGQ5MCIsInR5cCI6IkpXVCJ9.eyJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MjQ3MzI3MX1dLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwiYXVkIjoiYXV0aGVudGljYXRlZCIsImVtYWlsIjoiZ3VpbGxlcm1vLmFycmlldGFAbGFzYWxsZS5teCIsImV4cCI6MTc3MjQ4MDM4MSwiZmVhdHVyZV94Ijp0cnVlLCJpYXQiOjE3NzI0NzY3ODEsImlzX2Fub255bW91cyI6ZmFsc2UsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTQzMjEvYXV0aC92MSIsInBob25lIjoiIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJzZXNzaW9uX2lkIjoiZjc2ZjRkZGYtZTcxOS00NjRiLWIwYWUtODBhNmIxMjBlZmE0Iiwic3ViIjoiOTVkMWYzODEtNzAyMy00ZjMzLWJjNTQtMDhjY2QwOGFiZjQxIiwidGVuYW50X2lkIjoibG9jYWwtZGV2LXRlbmFudCIsInVzZXJfbWV0YWRhdGEiOnsiZW1haWxfdmVyaWZpZWQiOnRydWV9fQ.28CQUM9O7Cn1DlbMsLLaN-19uDmEGC_NvLRL6S3-Z3ym3T5gQ5OXiJ6_NlyblMJfJOQdMo88uGRIAZt9PwkE6A\",\n",
|
||||
" token_type: \"bearer\",\n",
|
||||
" expires_in: 3600,\n",
|
||||
" expires_at: 1772480381,\n",
|
||||
" refresh_token: \"363vd67k2taw\",\n",
|
||||
" user: {\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" aud: \"authenticated\",\n",
|
||||
" role: \"authenticated\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" phone: \"\",\n",
|
||||
" confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" last_sign_in_at: \"2026-03-02T17:41:10.986584Z\",\n",
|
||||
" app_metadata: { provider: \"email\", providers: [ \"email\" ] },\n",
|
||||
" user_metadata: { email_verified: true },\n",
|
||||
" identities: [\n",
|
||||
" {\n",
|
||||
" identity_id: \"b2f3094d-a29b-42b5-be69-3397282ffcd7\",\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" user_id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" identity_data: {\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_verified: false,\n",
|
||||
" phone_verified: false,\n",
|
||||
" sub: \"95d1f381-7023-4f33-bc54-08ccd08abf41\"\n",
|
||||
" },\n",
|
||||
" provider: \"email\",\n",
|
||||
" last_sign_in_at: \"2026-01-13T14:54:39.085382Z\",\n",
|
||||
" created_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" updated_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\"\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" created_at: \"2026-01-13T14:54:39.081453Z\",\n",
|
||||
" updated_at: \"2026-03-02T18:39:41.80176Z\",\n",
|
||||
" is_anonymous: false\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"TOKEN_REFRESHED {\n",
|
||||
" aal: \"aal1\",\n",
|
||||
" amr: [ { method: \"password\", timestamp: 1772473271 } ],\n",
|
||||
" app_metadata: { provider: \"email\", providers: [ \"email\" ] },\n",
|
||||
" aud: \"authenticated\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" exp: 1772483893,\n",
|
||||
" feature_x: true,\n",
|
||||
" iat: 1772480293,\n",
|
||||
" is_anonymous: false,\n",
|
||||
" iss: \"http://127.0.0.1:54321/auth/v1\",\n",
|
||||
" phone: \"\",\n",
|
||||
" role: \"authenticated\",\n",
|
||||
" session_id: \"f76f4ddf-e719-464b-b0ae-80a6b120efa4\",\n",
|
||||
" sub: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" tenant_id: \"local-dev-tenant\",\n",
|
||||
" user_metadata: { email_verified: true }\n",
|
||||
"}\n",
|
||||
"{\n",
|
||||
" access_token: \"eyJhbGciOiJFUzI1NiIsImtpZCI6ImI4MTI2OWYxLTIxZDgtNGYyZS1iNzE5LWMyMjQwYTg0MGQ5MCIsInR5cCI6IkpXVCJ9.eyJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MjQ3MzI3MX1dLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwiYXVkIjoiYXV0aGVudGljYXRlZCIsImVtYWlsIjoiZ3VpbGxlcm1vLmFycmlldGFAbGFzYWxsZS5teCIsImV4cCI6MTc3MjQ4Mzg5MywiZmVhdHVyZV94Ijp0cnVlLCJpYXQiOjE3NzI0ODAyOTMsImlzX2Fub255bW91cyI6ZmFsc2UsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTQzMjEvYXV0aC92MSIsInBob25lIjoiIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJzZXNzaW9uX2lkIjoiZjc2ZjRkZGYtZTcxOS00NjRiLWIwYWUtODBhNmIxMjBlZmE0Iiwic3ViIjoiOTVkMWYzODEtNzAyMy00ZjMzLWJjNTQtMDhjY2QwOGFiZjQxIiwidGVuYW50X2lkIjoibG9jYWwtZGV2LXRlbmFudCIsInVzZXJfbWV0YWRhdGEiOnsiZW1haWxfdmVyaWZpZWQiOnRydWV9fQ.TUYrfJrHy3fwPwI4VD24oYunaNAehDMnJleKn_JQg5LHwBIqe422qlGkNXvhu7juknS-kcEjJ1MaXBAP4Cu_WQ\",\n",
|
||||
" token_type: \"bearer\",\n",
|
||||
" expires_in: 3600,\n",
|
||||
" expires_at: 1772483893,\n",
|
||||
" refresh_token: \"s35spp4fy6oe\",\n",
|
||||
" user: {\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" aud: \"authenticated\",\n",
|
||||
" role: \"authenticated\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" phone: \"\",\n",
|
||||
" confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" last_sign_in_at: \"2026-03-02T17:41:10.986584Z\",\n",
|
||||
" app_metadata: { provider: \"email\", providers: [ \"email\" ] },\n",
|
||||
" user_metadata: { email_verified: true },\n",
|
||||
" identities: [\n",
|
||||
" {\n",
|
||||
" identity_id: \"b2f3094d-a29b-42b5-be69-3397282ffcd7\",\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" user_id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" identity_data: {\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_verified: false,\n",
|
||||
" phone_verified: false,\n",
|
||||
" sub: \"95d1f381-7023-4f33-bc54-08ccd08abf41\"\n",
|
||||
" },\n",
|
||||
" provider: \"email\",\n",
|
||||
" last_sign_in_at: \"2026-01-13T14:54:39.085382Z\",\n",
|
||||
" created_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" updated_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\"\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" created_at: \"2026-01-13T14:54:39.081453Z\",\n",
|
||||
" updated_at: \"2026-03-02T19:38:13.130638Z\",\n",
|
||||
" is_anonymous: false\n",
|
||||
" }\n",
|
||||
"}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import { jwtDecode } from \"npm:jwt-decode\";\n",
|
||||
"\n",
|
||||
"const listener = supabase.auth.onAuthStateChange(async (event, session) => {\n",
|
||||
" if (session) {\n",
|
||||
" const jwt = jwtDecode(session.access_token);\n",
|
||||
" console.log(event, jwt);\n",
|
||||
" // store in the session\n",
|
||||
" localStorage.setItem(\"jwt\", JSON.stringify(session));\n",
|
||||
"\n",
|
||||
" }\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"listener;\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 35,
|
||||
"id": "0471a2d4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{\n",
|
||||
" access_token: \"eyJhbGciOiJFUzI1NiIsImtpZCI6ImI4MTI2OWYxLTIxZDgtNGYyZS1iNzE5LWMyMjQwYTg0MGQ5MCIsInR5cCI6IkpXVCJ9.eyJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MjQ1OTQzMX1dLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwiYXVkIjoiYXV0aGVudGljYXRlZCIsImVtYWlsIjoiZ3VpbGxlcm1vLmFycmlldGFAbGFzYWxsZS5teCIsImV4cCI6MTc3MjQ3MjAwMywiZmVhdHVyZV94Ijp0cnVlLCJpYXQiOjE3NzI0Njg0MDMsImlzX2Fub255bW91cyI6ZmFsc2UsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTQzMjEvYXV0aC92MSIsInBob25lIjoiIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJzZXNzaW9uX2lkIjoiMTJkNDc0OGUtNDBiMy00MzQ5LWI0MjUtMWFkNzM2YTk2NTM5Iiwic3ViIjoiOTVkMWYzODEtNzAyMy00ZjMzLWJjNTQtMDhjY2QwOGFiZjQxIiwidGVuYW50X2lkIjoibG9jYWwtZGV2LXRlbmFudCIsInVzZXJfbWV0YWRhdGEiOnsiZW1haWxfdmVyaWZpZWQiOnRydWV9fQ.XbxmaKPEo9Vqr1_W1vuXIaYwGiifaBsaqIwFdJvIuMrFPx5VeW9-NeUasWq3ka1DBsEMWaCFeIKykp9QlvDXqQ\",\n",
|
||||
" token_type: \"bearer\",\n",
|
||||
" expires_in: 3600,\n",
|
||||
" expires_at: 1772472003,\n",
|
||||
" refresh_token: \"gjdfzz4y4y3v\",\n",
|
||||
" user: {\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" aud: \"authenticated\",\n",
|
||||
" role: \"authenticated\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" phone: \"\",\n",
|
||||
" confirmed_at: \"2026-01-13T14:54:39.096101Z\",\n",
|
||||
" last_sign_in_at: \"2026-03-02T13:50:31.464916Z\",\n",
|
||||
" app_metadata: { provider: \"email\", providers: [ \"email\" ] },\n",
|
||||
" user_metadata: { email_verified: true },\n",
|
||||
" identities: [\n",
|
||||
" {\n",
|
||||
" identity_id: \"b2f3094d-a29b-42b5-be69-3397282ffcd7\",\n",
|
||||
" id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" user_id: \"95d1f381-7023-4f33-bc54-08ccd08abf41\",\n",
|
||||
" identity_data: {\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\",\n",
|
||||
" email_verified: false,\n",
|
||||
" phone_verified: false,\n",
|
||||
" sub: \"95d1f381-7023-4f33-bc54-08ccd08abf41\"\n",
|
||||
" },\n",
|
||||
" provider: \"email\",\n",
|
||||
" last_sign_in_at: \"2026-01-13T14:54:39.085382Z\",\n",
|
||||
" created_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" updated_at: \"2026-01-13T14:54:39.08544Z\",\n",
|
||||
" email: \"guillermo.arrieta@lasalle.mx\"\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" created_at: \"2026-01-13T14:54:39.081453Z\",\n",
|
||||
" updated_at: \"2026-03-02T16:20:03.535209Z\",\n",
|
||||
" is_anonymous: false\n",
|
||||
" }\n",
|
||||
"}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"const raw = localStorage.getItem(\"jwt\")\n",
|
||||
"const jwt = raw ? JSON.parse(raw) : null\n",
|
||||
"\n",
|
||||
"console.log(jwt)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 36,
|
||||
"id": "0bfcd412",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "ReferenceError",
|
||||
"evalue": "useEffect is not defined",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"Stack trace:",
|
||||
"ReferenceError: useEffect is not defined",
|
||||
" at <anonymous>:1:22"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"useEffect(() => {\n",
|
||||
" const onFocus = () => supabase.auth.refreshSession();\n",
|
||||
" window.addEventListener(\"focus\", onFocus);\n",
|
||||
" return () => window.removeEventListener(\"focus\", onFocus);\n",
|
||||
"}, []);"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "88d2d6bc",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Deno",
|
||||
"language": "typescript",
|
||||
"name": "deno"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": "typescript",
|
||||
"file_extension": ".ts",
|
||||
"mimetype": "text/x.typescript",
|
||||
"name": "typescript",
|
||||
"nbconvert_exporter": "script",
|
||||
"pygments_lexer": "typescript",
|
||||
"version": "5.9.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
+4
-1
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"@toon-format/toon": "^2.1.0",
|
||||
"citeproc": "^2.4.63",
|
||||
"deno": "^2.6.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"openai": "^6.16.0",
|
||||
"supabase": "^2.72.6"
|
||||
"supabase": "2.75.0"
|
||||
},
|
||||
"name": "genesis-2",
|
||||
"module": "index.ts",
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.185.0
|
||||
v2.187.0
|
||||
@@ -1 +1 @@
|
||||
buckets-objects-grants-postgres
|
||||
fix-optimized-search-function
|
||||
@@ -1 +1 @@
|
||||
v1.33.0
|
||||
v1.37.7
|
||||
@@ -1,3 +1,5 @@
|
||||
[db]
|
||||
major_version = 15
|
||||
|
||||
[functions.ai-generate-plan]
|
||||
enabled = true
|
||||
@@ -48,3 +50,29 @@ entrypoint = "./functions/generate-subject-suggestions/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/generate-subject-suggestions/*.html" ]
|
||||
|
||||
[auth.hook.custom_access_token]
|
||||
enabled = true
|
||||
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
||||
|
||||
[functions.openai-webhook-responses]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
import_map = "./functions/openai-webhook-responses/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
entrypoint = "./functions/openai-webhook-responses/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/openai-webhook-responses/*.html" ]
|
||||
|
||||
[functions.buscar-bibliografia]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
import_map = "./functions/buscar-bibliografia/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
entrypoint = "./functions/buscar-bibliografia/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/buscar-bibliografia/*.html" ]
|
||||
|
||||
+2481
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
export type Json =
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
@@ -460,6 +460,7 @@ export type Database = {
|
||||
estado: Database["public"]["Enums"]["estado_conversacion"]
|
||||
id: string
|
||||
intento_archivado: number
|
||||
nombre: string | null
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
@@ -472,6 +473,7 @@ export type Database = {
|
||||
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
nombre?: string | null
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
@@ -484,6 +486,7 @@ export type Database = {
|
||||
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
nombre?: string | null
|
||||
openai_conversation_id?: string
|
||||
plan_estudio_id?: string
|
||||
}
|
||||
@@ -795,6 +798,27 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
person: {
|
||||
Row: {
|
||||
email: string | null
|
||||
first_name: string | null
|
||||
id: number
|
||||
last_name: string | null
|
||||
}
|
||||
Insert: {
|
||||
email?: string | null
|
||||
first_name?: string | null
|
||||
id?: number
|
||||
last_name?: string | null
|
||||
}
|
||||
Update: {
|
||||
email?: string | null
|
||||
first_name?: string | null
|
||||
id?: number
|
||||
last_name?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
planes_estudio: {
|
||||
Row: {
|
||||
activo: boolean
|
||||
@@ -901,6 +925,36 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
professor: {
|
||||
Row: {
|
||||
department: string | null
|
||||
email: string | null
|
||||
employee_number: string | null
|
||||
first_name: string | null
|
||||
id: number
|
||||
last_name: string | null
|
||||
salary: number | null
|
||||
}
|
||||
Insert: {
|
||||
department?: string | null
|
||||
email?: string | null
|
||||
employee_number?: string | null
|
||||
first_name?: string | null
|
||||
id?: number
|
||||
last_name?: string | null
|
||||
salary?: number | null
|
||||
}
|
||||
Update: {
|
||||
department?: string | null
|
||||
email?: string | null
|
||||
employee_number?: string | null
|
||||
first_name?: string | null
|
||||
id?: number
|
||||
last_name?: string | null
|
||||
salary?: number | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
responsables_asignatura: {
|
||||
Row: {
|
||||
asignatura_id: string
|
||||
@@ -961,6 +1015,36 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
student: {
|
||||
Row: {
|
||||
email: string | null
|
||||
enrollment_number: string | null
|
||||
first_name: string | null
|
||||
id: number
|
||||
last_name: string | null
|
||||
major: string | null
|
||||
semester: number | null
|
||||
}
|
||||
Insert: {
|
||||
email?: string | null
|
||||
enrollment_number?: string | null
|
||||
first_name?: string | null
|
||||
id?: number
|
||||
last_name?: string | null
|
||||
major?: string | null
|
||||
semester?: number | null
|
||||
}
|
||||
Update: {
|
||||
email?: string | null
|
||||
enrollment_number?: string | null
|
||||
first_name?: string | null
|
||||
id?: number
|
||||
last_name?: string | null
|
||||
major?: string | null
|
||||
semester?: number | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
tareas_revision: {
|
||||
Row: {
|
||||
asignado_a: string
|
||||
@@ -1206,6 +1290,15 @@ export type Database = {
|
||||
}
|
||||
}
|
||||
Functions: {
|
||||
append_conversacion_asignatura: {
|
||||
Args: { p_append: Json; p_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
append_conversacion_plan: {
|
||||
Args: { p_append: Json; p_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
custom_access_token_hook: { Args: { event: Json }; Returns: Json }
|
||||
unaccent: { Args: { "": string }; Returns: string }
|
||||
unaccent_immutable: { Args: { "": string }; Returns: string }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/// <reference lib="deno.window" />
|
||||
import OpenAI from "npm:openai@6.16.0";
|
||||
import type * as OpenAITypes from "npm:openai@6.16.0";
|
||||
import { createClient, type SupabaseClient } from "npm:@supabase/supabase-js@2";
|
||||
// Use non-streaming params to ensure `responses.create` returns a typed Response
|
||||
export type StructuredResponseOptions =
|
||||
OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming;
|
||||
@@ -15,7 +14,6 @@ export type StructuredResponseSuccess<TOutput = unknown> = {
|
||||
responseId: string;
|
||||
conversationId?: string | null;
|
||||
references: {
|
||||
uploadedToStorage: string[]; // supabase storage paths
|
||||
openaiFileIds: string[]; // file ids in OpenAI
|
||||
};
|
||||
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
|
||||
@@ -24,7 +22,6 @@ export type StructuredResponseFailure = {
|
||||
ok: false;
|
||||
code:
|
||||
| "MissingEnv"
|
||||
| "StorageUploadFailed"
|
||||
| "OpenAIFileUploadFailed"
|
||||
| "OpenAIRequestFailed";
|
||||
message: string;
|
||||
@@ -34,40 +31,24 @@ export type StructuredResponseResult<TOutput = unknown> =
|
||||
| StructuredResponseSuccess<TOutput>
|
||||
| StructuredResponseFailure;
|
||||
export interface OpenAIServiceConfig {
|
||||
supabaseUrl: string;
|
||||
serviceRoleKey: string;
|
||||
openAIApiKey: string;
|
||||
bucket?: string; // default: ai-storage
|
||||
}
|
||||
export class OpenAIService {
|
||||
private readonly supabase: SupabaseClient;
|
||||
private readonly openai: OpenAI;
|
||||
private readonly bucket: string;
|
||||
private constructor(
|
||||
client: SupabaseClient,
|
||||
openai: OpenAI,
|
||||
bucket: string,
|
||||
) {
|
||||
this.supabase = client;
|
||||
private constructor(openai: OpenAI) {
|
||||
this.openai = openai;
|
||||
this.bucket = bucket;
|
||||
}
|
||||
static fromEnv(): StructuredResponseFailure | OpenAIService {
|
||||
const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
|
||||
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
||||
const openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? "";
|
||||
const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage";
|
||||
if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) {
|
||||
if (!openAIApiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "MissingEnv",
|
||||
message:
|
||||
"Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY",
|
||||
message: "Required env vars missing: OPENAI_API_KEY",
|
||||
};
|
||||
}
|
||||
const client = createClient(supabaseUrl, serviceRoleKey);
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey });
|
||||
return new OpenAIService(client, openai, bucket);
|
||||
return new OpenAIService(openai);
|
||||
}
|
||||
async createConversation(metadata?: Record<string, string>) {
|
||||
const conversation = await this.openai.conversations.create({
|
||||
@@ -80,9 +61,6 @@ export class OpenAIService {
|
||||
files?: File[],
|
||||
): Promise<StructuredResponseResult<TOutput>> {
|
||||
try {
|
||||
const uploadedToStorage = await this.uploadFilesToStorage(
|
||||
files ?? [],
|
||||
);
|
||||
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
|
||||
|
||||
const newOptions = { ...options };
|
||||
@@ -112,6 +90,11 @@ export class OpenAIService {
|
||||
const openaiRaw = (await this.openai.responses.create(
|
||||
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
||||
)) as OpenAITypes.OpenAI.Responses.Response;
|
||||
|
||||
const isBackground =
|
||||
(newOptions as unknown as { background?: boolean })
|
||||
.background ===
|
||||
true;
|
||||
const { model, id: responseId } = openaiRaw;
|
||||
const usage = openaiRaw?.usage ?? null;
|
||||
const conversationId = (
|
||||
@@ -119,6 +102,22 @@ export class OpenAIService {
|
||||
conversation_id?: string | null;
|
||||
}
|
||||
).conversation_id ?? null;
|
||||
|
||||
if (isBackground) {
|
||||
return {
|
||||
ok: true,
|
||||
output: undefined,
|
||||
outputText: undefined,
|
||||
model: String(model),
|
||||
usage,
|
||||
responseId: String(responseId),
|
||||
conversationId: conversationId
|
||||
? String(conversationId)
|
||||
: null,
|
||||
references: { openaiFileIds },
|
||||
openaiRaw,
|
||||
};
|
||||
}
|
||||
// Try to read structured JSON output
|
||||
let output: TOutput | undefined = undefined;
|
||||
let outputText: string | undefined = undefined;
|
||||
@@ -154,38 +153,18 @@ export class OpenAIService {
|
||||
usage,
|
||||
responseId: String(responseId),
|
||||
conversationId: conversationId ? String(conversationId) : null,
|
||||
references: { uploadedToStorage, openaiFileIds },
|
||||
references: { openaiFileIds },
|
||||
openaiRaw,
|
||||
};
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
const message = e?.message ?? "Unknown error";
|
||||
const code = message.includes("Supabase upload failed")
|
||||
? "StorageUploadFailed"
|
||||
: message.includes("OpenAI file upload failed")
|
||||
const code = message.includes("OpenAI file upload failed")
|
||||
? "OpenAIFileUploadFailed"
|
||||
: "OpenAIRequestFailed";
|
||||
return { ok: false, code, message, cause: err };
|
||||
}
|
||||
}
|
||||
private async uploadFilesToStorage(files: File[]): Promise<string[]> {
|
||||
const paths: string[] = [];
|
||||
for (const file of files) {
|
||||
const safeName = this.sanitizeFilename(file.name);
|
||||
const path = `${crypto.randomUUID()}-${safeName}`;
|
||||
const { data, error } = await this.supabase.storage
|
||||
.from(this.bucket)
|
||||
.upload(path, file, {
|
||||
contentType: file.type || "application/octet-stream",
|
||||
upsert: false,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(`Supabase upload failed: ${error.message}`);
|
||||
}
|
||||
paths.push(data.path);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
private async uploadFilesToOpenAI(files: File[]): Promise<string[]> {
|
||||
const ids: string[] = [];
|
||||
for (const file of files) {
|
||||
|
||||
@@ -47,3 +47,9 @@ export function sendError(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export interface ResponseMetadata extends Record<string, string | undefined> {
|
||||
tabla?: string;
|
||||
accion?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ type NivelType =
|
||||
type TipoCicloType =
|
||||
Database["public"]["Tables"]["planes_estudio"]["Insert"]["tipo_ciclo"];
|
||||
|
||||
type BeforeUnloadWithDetail = Event & { detail?: { reason?: unknown } };
|
||||
|
||||
// Re-registramos con tipo estricto (evita `any` en análisis)
|
||||
addEventListener("beforeunload", (ev: BeforeUnloadWithDetail) => {
|
||||
console.error("ALERTA: La función se va a apagar. Razón:", ev.detail?.reason);
|
||||
});
|
||||
|
||||
Deno.serve(async (req: Request): Promise<Response> => {
|
||||
const url = new URL(req.url);
|
||||
const functionName = url.pathname.split("/").pop();
|
||||
@@ -118,6 +125,10 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
SERVICE_ROLE_KEY,
|
||||
);
|
||||
|
||||
// Model name controlled via env var (single use)
|
||||
const AI_GENERATE_PLAN_MODELO = Deno.env.get("AI_GENERATE_PLAN_MODELO") ??
|
||||
"gpt-5-nano";
|
||||
|
||||
const formData = await req.formData();
|
||||
const validation = parseAndValidate(formData);
|
||||
if (!validation.success) {
|
||||
@@ -184,8 +195,98 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
? estructuraPlan.definicion as Record<string, unknown>
|
||||
: {};
|
||||
|
||||
const { data: estado } = await supabaseService
|
||||
.from("estados_plan")
|
||||
.select("id,clave,orden")
|
||||
.eq("clave", "GENERANDO")
|
||||
.maybeSingle();
|
||||
|
||||
if (!estado?.id) {
|
||||
throw new HttpError(
|
||||
500,
|
||||
"No se encontró el estado GENERANDO.",
|
||||
"MISSING_STATE",
|
||||
{ clave: "GENERANDO" },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: carrera, error: carreraError } = await supabaseService
|
||||
.from("carreras")
|
||||
.select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)")
|
||||
.eq("id", payload.datosBasicos.carreraId)
|
||||
.maybeSingle();
|
||||
if (carreraError) {
|
||||
throw new HttpError(
|
||||
500,
|
||||
"No se pudo obtener la carrera.",
|
||||
"SUPABASE_QUERY_FAILED",
|
||||
carreraError,
|
||||
);
|
||||
}
|
||||
if (!carrera) {
|
||||
throw new HttpError(
|
||||
404,
|
||||
"No se encontró la carrera.",
|
||||
"NOT_FOUND",
|
||||
{ table: "carreras", id: payload.datosBasicos.carreraId },
|
||||
);
|
||||
}
|
||||
|
||||
const planInsert: Database["public"]["Tables"]["planes_estudio"]["Insert"] =
|
||||
{
|
||||
carrera_id: carrera.id as string,
|
||||
estructura_id: estructuraPlan?.id as string,
|
||||
nombre: payload.datosBasicos.nombrePlan,
|
||||
nivel: payload.datosBasicos.nivel as NivelType,
|
||||
tipo_ciclo: payload.datosBasicos.tipoCiclo as TipoCicloType,
|
||||
numero_ciclos: payload.datosBasicos.numCiclos,
|
||||
// IMPORTANTE: se inserta SIN `datos` (se actualiza vía webhook)
|
||||
estado_actual_id: estado.id,
|
||||
activo: true,
|
||||
tipo_origen: "IA",
|
||||
meta_origen: {
|
||||
generado_por: "ai-generate-plan",
|
||||
referencias: {
|
||||
archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null,
|
||||
repositoriosIds: payload.iaConfig?.repositoriosIds ?? null,
|
||||
},
|
||||
iaConfig: {
|
||||
descripcionEnfoqueAcademico:
|
||||
payload.iaConfig?.descripcionEnfoqueAcademico ?? null,
|
||||
instruccionesAdicionalesIA:
|
||||
payload.iaConfig?.instruccionesAdicionalesIA ?? null,
|
||||
usarMCP: Boolean(payload.iaConfig?.usarMCP),
|
||||
},
|
||||
} as unknown as Json,
|
||||
};
|
||||
|
||||
const { data: plan, error: planError } = await supabaseService
|
||||
.from("planes_estudio")
|
||||
.insert(planInsert)
|
||||
.select(
|
||||
"id,nombre,nivel,tipo_ciclo,numero_ciclos,carrera_id,estructura_id,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,datos",
|
||||
)
|
||||
.single();
|
||||
|
||||
if (planError) {
|
||||
const maybeCode = (planError as { code?: string }).code;
|
||||
const status = maybeCode ? 409 : 500;
|
||||
throw new HttpError(
|
||||
status,
|
||||
"No se pudo guardar el plan de estudios.",
|
||||
"SUPABASE_INSERT_FAILED",
|
||||
{ ...planError, code: maybeCode },
|
||||
);
|
||||
}
|
||||
|
||||
const aiStructuredPayload: StructuredResponseOptions = {
|
||||
model: "gpt-5-nano",
|
||||
model: AI_GENERATE_PLAN_MODELO,
|
||||
background: true,
|
||||
metadata: {
|
||||
tabla: "planes_estudio",
|
||||
accion: "crear",
|
||||
id: String(plan.id),
|
||||
},
|
||||
input: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
@@ -233,126 +334,12 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
const status = aiResult.code === "MissingEnv" ? 500 : 502;
|
||||
throw new HttpError(
|
||||
status,
|
||||
"No se pudo generar el plan con IA.",
|
||||
"No se pudo iniciar la generación del plan con IA.",
|
||||
"OPENAI_REQUEST_FAILED",
|
||||
aiResult,
|
||||
);
|
||||
}
|
||||
|
||||
// Prefer parsed output; fallback to parse outputText
|
||||
let aiOutput = aiResult.output ?? null;
|
||||
if (aiOutput == null && aiResult.outputText) {
|
||||
try {
|
||||
aiOutput = JSON.parse(aiResult.outputText);
|
||||
} catch {
|
||||
throw new HttpError(
|
||||
502,
|
||||
"La respuesta de la IA no es JSON válido.",
|
||||
"OPENAI_INVALID_JSON",
|
||||
{ outputText: aiResult.outputText },
|
||||
);
|
||||
}
|
||||
}
|
||||
const aiOutputJson: Json = aiOutput as unknown as Json;
|
||||
if (!aiOutput) {
|
||||
throw new HttpError(
|
||||
502,
|
||||
"La respuesta de la IA no contiene salida estructurada.",
|
||||
"OPENAI_MISSING_STRUCTURED_OUTPUT",
|
||||
{ outputText: aiResult.outputText ?? null },
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Insertar interacciones con IA y quizas forzar a que la informacion de datosBasicos sea la misma que la recibida
|
||||
|
||||
const { data: estado } = await supabaseService
|
||||
.from("estados_plan")
|
||||
.select("id,clave,orden")
|
||||
.ilike("clave", "BORRADOR%")
|
||||
.order("orden", { ascending: true })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
const { data: carrera, error: carreraError } = await supabaseService
|
||||
.from("carreras")
|
||||
.select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)")
|
||||
.eq("id", payload.datosBasicos.carreraId)
|
||||
.maybeSingle();
|
||||
if (carreraError) {
|
||||
throw new HttpError(
|
||||
500,
|
||||
"No se pudo obtener la carrera.",
|
||||
"SUPABASE_QUERY_FAILED",
|
||||
carreraError,
|
||||
);
|
||||
}
|
||||
if (!carrera) {
|
||||
throw new HttpError(
|
||||
404,
|
||||
"No se encontró la carrera.",
|
||||
"NOT_FOUND",
|
||||
{ table: "carreras", id: payload.datosBasicos.carreraId },
|
||||
);
|
||||
}
|
||||
|
||||
const planInsert: Database["public"]["Tables"]["planes_estudio"]["Insert"] =
|
||||
{
|
||||
carrera_id: carrera?.id as string,
|
||||
estructura_id: estructuraPlan?.id as string,
|
||||
nombre: payload.datosBasicos.nombrePlan,
|
||||
nivel: payload.datosBasicos.nivel as NivelType,
|
||||
tipo_ciclo: payload.datosBasicos.tipoCiclo as TipoCicloType,
|
||||
numero_ciclos: payload.datosBasicos.numCiclos,
|
||||
datos: aiOutputJson,
|
||||
estado_actual_id: estado?.id ?? undefined,
|
||||
activo: true,
|
||||
tipo_origen: "IA",
|
||||
meta_origen: {
|
||||
generado_por: "ai_generate_plan",
|
||||
ai_structured: {
|
||||
responseId: aiResult.responseId ?? null,
|
||||
conversationId: aiResult.conversationId ?? null,
|
||||
model: aiResult.model,
|
||||
usage: aiResult.usage ?? null,
|
||||
},
|
||||
referencias: {
|
||||
archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null,
|
||||
repositoriosIds: payload.iaConfig?.repositoriosIds ?? null,
|
||||
},
|
||||
iaConfig: {
|
||||
descripcionEnfoqueAcademico:
|
||||
payload.iaConfig?.descripcionEnfoqueAcademico ?? null,
|
||||
instruccionesAdicionalesIA:
|
||||
payload.iaConfig?.instruccionesAdicionalesIA ?? null,
|
||||
usarMCP: Boolean(payload.iaConfig?.usarMCP),
|
||||
},
|
||||
} as unknown as Json,
|
||||
};
|
||||
|
||||
const { data: plan, error: planError } = await supabaseService
|
||||
.from("planes_estudio")
|
||||
.insert(planInsert)
|
||||
.select(
|
||||
"id,nombre,nivel,tipo_ciclo,numero_ciclos,carrera_id,estructura_id,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,datos",
|
||||
)
|
||||
.single();
|
||||
|
||||
// TODO: interaccion con IA y cambio de estado
|
||||
|
||||
if (planError) {
|
||||
const maybeCode = (planError as { code?: string }).code;
|
||||
// Common cases:
|
||||
// - foreign key / constraint violations -> 409
|
||||
// - others -> 500
|
||||
const status = maybeCode ? 409 : 500;
|
||||
throw new HttpError(
|
||||
status,
|
||||
"No se pudo guardar el plan de estudios.",
|
||||
"SUPABASE_INSERT_FAILED",
|
||||
{ ...planError, code: maybeCode },
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: update a interaccion_ia y e insert a cambios_plan con id de plan generado
|
||||
|
||||
console.log(
|
||||
|
||||
@@ -34,4 +34,13 @@ Antes de ejecutar el trabajo, comienza con una lista concisa (3-7 puntos) de los
|
||||
|
||||
# Validación de Salida
|
||||
Después de generar el objeto, realiza una validación breve para asegurar que todos los campos requeridos estén presentes, que los valores cumplan los criterios normativos y que la estructura cumpla con el json_schema indicado.
|
||||
Corrige cualquier inconsistencia detectada antes de finalizar la entrega.`;
|
||||
Corrige cualquier inconsistencia detectada antes de finalizar la entrega.
|
||||
|
||||
# REGLAS DE FORMATO PARA CAMPOS DE TEXTO (STRING)
|
||||
1. **Prioridad:** Estas reglas aplican por defecto. Si el prompt del usuario solicita explícitamente un formato distinto, esa instrucción tiene prioridad sobre estas reglas.
|
||||
2. **Estilo Visual:** Redacta el contenido exclusivamente para visualización en texto plano (estilo 'white-space: pre-wrap').
|
||||
3. **Estructura Vertical:** Utiliza saltos de línea explícitos para romper líneas y doble salto de línea para separar párrafos.
|
||||
4. **Indentación Estricta:** Usa exactamente 2 espacios para la indentación jerárquica. No uses tabuladores.
|
||||
5. **Listas:** Utiliza un guion seguido de un espacio ("- ") para los elementos de lista.
|
||||
6. **Prohibiciones:** No incluyas etiquetas HTML, sintaxis Markdown ni caracteres de escape literales visibles en el texto final. Asegúrate de que el JSON final contenga saltos de línea válidos ('\n') y no texto escapado.
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,24 @@
|
||||
import { Tables } from "../_shared/database.types.ts";
|
||||
|
||||
export interface UploadedFile {
|
||||
id: string; // Necesario para React (key)
|
||||
file: File; // La fuente de verdad (contiene name, size, type)
|
||||
preview?: string; // Opcional: si fueran imágenes
|
||||
}
|
||||
|
||||
export type AIGenerateSubjectInput = {
|
||||
plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
|
||||
datosBasicos: {
|
||||
nombre: Tables<"asignaturas">["nombre"];
|
||||
datosUpdate: {
|
||||
id?: Tables<"asignaturas">["id"]; // si viene, es update; si no, es insert
|
||||
plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
|
||||
estructura_id?: Tables<"asignaturas">["estructura_id"] | null;
|
||||
|
||||
nombre?: Tables<"asignaturas">["nombre"];
|
||||
codigo?: Tables<"asignaturas">["codigo"];
|
||||
tipo: Tables<"asignaturas">["tipo"] | null;
|
||||
creditos: Tables<"asignaturas">["creditos"] | null;
|
||||
horasAcademicas?: Tables<"asignaturas">["horas_academicas"] | null;
|
||||
horasIndependientes?: Tables<"asignaturas">["horas_independientes"] | null;
|
||||
estructuraId: Tables<"asignaturas">["estructura_id"] | null;
|
||||
tipo?: Tables<"asignaturas">["tipo"] | null;
|
||||
creditos?: Tables<"asignaturas">["creditos"] | null;
|
||||
horas_academicas?: Tables<"asignaturas">["horas_academicas"] | null;
|
||||
horas_independientes?: Tables<"asignaturas">["horas_independientes"] | null;
|
||||
numero_ciclo?: Tables<"asignaturas">["numero_ciclo"] | null;
|
||||
linea_plan_id?: Tables<"asignaturas">["linea_plan_id"] | null;
|
||||
orden_celda?: Tables<"asignaturas">["orden_celda"] | null;
|
||||
};
|
||||
// clonInterno?: {
|
||||
// facultadId?: string
|
||||
// carreraId?: string
|
||||
// planOrigenId?: string
|
||||
// asignaturaOrigenId?: string | null
|
||||
// }
|
||||
// clonTradicional?: {
|
||||
// archivoWordAsignaturaId: string | null
|
||||
// archivosAdicionalesIds: Array<string>
|
||||
// }
|
||||
iaConfig?: {
|
||||
descripcionEnfoqueAcademico: string;
|
||||
instruccionesAdicionalesIA: string;
|
||||
archivosReferencia: Array<string>;
|
||||
repositoriosReferencia?: Array<string>;
|
||||
descripcionEnfoqueAcademico?: string;
|
||||
instruccionesAdicionalesIA?: string;
|
||||
archivosAdjuntos?: string[]; // rutas (no File)
|
||||
};
|
||||
archivosAdjuntos?: Array<UploadedFile>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Configuration for private npm package dependencies
|
||||
# For more information on using private registries with Edge Functions, see:
|
||||
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"imports": {
|
||||
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { z } from "zod";
|
||||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
||||
|
||||
console.log("Starting buscar-bibliografia function");
|
||||
|
||||
type GoogleBooksVolume = Record<string, unknown>;
|
||||
type OpenLibraryDoc = Record<string, unknown>;
|
||||
|
||||
type EndpointResult =
|
||||
| { endpoint: "google"; item: GoogleBooksVolume }
|
||||
| { endpoint: "open_library"; item: OpenLibraryDoc };
|
||||
|
||||
interface GoogleBooksVolumesListResponse {
|
||||
kind?: string;
|
||||
totalItems?: number;
|
||||
items?: GoogleBooksVolume[];
|
||||
}
|
||||
|
||||
interface OpenLibrarySearchResponse {
|
||||
num_found?: number;
|
||||
start?: number;
|
||||
docs?: OpenLibraryDoc[];
|
||||
}
|
||||
|
||||
const ISO6391 = z.string().regex(
|
||||
/^[a-z]{2}$/i,
|
||||
"Debe ser ISO 639-1 (2 letras)",
|
||||
);
|
||||
const ISO6392 = z.string().regex(
|
||||
/^[a-z]{3}$/i,
|
||||
"Debe ser ISO 639-2 (3 letras)",
|
||||
);
|
||||
|
||||
const SearchTermsSchema = z
|
||||
.object({
|
||||
q: z.string().min(1, "q es requerido"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const GoogleParamsSchema = z
|
||||
.object({
|
||||
orderBy: z.enum(["newest", "relevance"]).optional(),
|
||||
langRestrict: ISO6391.optional(),
|
||||
startIndex: z.number().int().min(0).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional()
|
||||
.default({});
|
||||
|
||||
const OpenLibraryParamsSchema = z
|
||||
.object({
|
||||
language: ISO6392.optional(),
|
||||
page: z.number().int().min(1).optional(),
|
||||
sort: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.optional()
|
||||
.default({});
|
||||
|
||||
const BodySchema = z
|
||||
.object({
|
||||
searchTerms: SearchTermsSchema,
|
||||
|
||||
// Parámetros por endpoint
|
||||
google: GoogleParamsSchema,
|
||||
openLibrary: OpenLibraryParamsSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
type BuscarBibliografiaRequest = z.infer<typeof BodySchema>;
|
||||
|
||||
function formatZodIssues(issues: z.ZodIssue[]): string {
|
||||
return issues
|
||||
.map((issue, i) => {
|
||||
const path = issue.path.length ? issue.path.join(".") : "(root)";
|
||||
return `${i + 1}. ${path}: ${issue.message}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function buildUrlWithSearchTerms(
|
||||
baseUrl: string,
|
||||
searchTerms: Record<string, unknown>,
|
||||
): string {
|
||||
const url = new URL(baseUrl);
|
||||
for (const [key, value] of Object.entries(searchTerms)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
if (v === undefined || v === null) continue;
|
||||
url.searchParams.append(key, String(v));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function pickSerializableParams(
|
||||
input: Record<string, unknown>,
|
||||
): Record<
|
||||
string,
|
||||
string | number | boolean | Array<string | number | boolean>
|
||||
> {
|
||||
const out: Record<
|
||||
string,
|
||||
string | number | boolean | Array<string | number | boolean>
|
||||
> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const arr = value
|
||||
.filter((v) => v !== undefined && v !== null)
|
||||
.filter((v) =>
|
||||
["string", "number", "boolean"].includes(typeof v)
|
||||
) as Array<
|
||||
string | number | boolean
|
||||
>;
|
||||
if (arr.length) out[key] = arr;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (["string", "number", "boolean"].includes(typeof value)) {
|
||||
out[key] = value as string | number | boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Deno.serve(async (req: Request): Promise<Response> => {
|
||||
const url = new URL(req.url);
|
||||
const functionName = url.pathname.split("/").pop();
|
||||
console.log(
|
||||
`[${new Date().toISOString()}][${functionName}]: Request received`,
|
||||
);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
throw new HttpError(405, "Método no permitido.", "METHOD_NOT_ALLOWED", {
|
||||
method: req.method,
|
||||
});
|
||||
}
|
||||
|
||||
const contentType = (req.headers.get("content-type") || "").toLowerCase();
|
||||
if (!contentType.includes("application/json")) {
|
||||
throw new HttpError(
|
||||
415,
|
||||
"Content-Type no soportado.",
|
||||
"UNSUPPORTED_MEDIA_TYPE",
|
||||
{ contentType, expected: "application/json" },
|
||||
);
|
||||
}
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await req.json();
|
||||
} catch (e) {
|
||||
throw new HttpError(400, "Body JSON inválido.", "INVALID_JSON", {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = BodySchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
throw new HttpError(
|
||||
422,
|
||||
formatZodIssues(parsed.error.issues),
|
||||
"VALIDATION_ERROR",
|
||||
parsed.error,
|
||||
);
|
||||
}
|
||||
|
||||
const body: BuscarBibliografiaRequest = parsed.data;
|
||||
|
||||
const GOOGLE_API_KEY = Deno.env.get("GOOGLE_API_KEY");
|
||||
if (!GOOGLE_API_KEY) {
|
||||
throw new HttpError(
|
||||
500,
|
||||
"Configuración del servidor incompleta.",
|
||||
"MISSING_ENV",
|
||||
{ missing: ["GOOGLE_API_KEY"] },
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = "https://www.googleapis.com/books/v1/volumes";
|
||||
const googleParams = pickSerializableParams({
|
||||
...body.google,
|
||||
...body.searchTerms,
|
||||
key: GOOGLE_API_KEY,
|
||||
maxResults: 20,
|
||||
startIndex: (body.google as Record<string, unknown>)?.startIndex ?? 0,
|
||||
});
|
||||
// No mandar parámetros exclusivos de Open Library a Google
|
||||
delete (googleParams as Record<string, unknown>)["page"];
|
||||
delete (googleParams as Record<string, unknown>)["language"];
|
||||
|
||||
const googleUrl = buildUrlWithSearchTerms(baseUrl, googleParams);
|
||||
|
||||
const openLibraryBaseUrl = "https://openlibrary.org/search.json";
|
||||
const openLibraryParams = pickSerializableParams({
|
||||
...body.openLibrary,
|
||||
q: body.searchTerms.q,
|
||||
limit: 20,
|
||||
page: (body.openLibrary as Record<string, unknown>)?.page ?? 1,
|
||||
});
|
||||
// No mandar parámetros exclusivos de Google a Open Library
|
||||
delete (openLibraryParams as Record<string, unknown>)["langRestrict"];
|
||||
delete (openLibraryParams as Record<string, unknown>)["orderBy"];
|
||||
delete (openLibraryParams as Record<string, unknown>)["startIndex"];
|
||||
|
||||
const openLibraryUrl = buildUrlWithSearchTerms(
|
||||
openLibraryBaseUrl,
|
||||
openLibraryParams,
|
||||
);
|
||||
|
||||
const [googleResp, openLibraryResp] = await Promise.all([
|
||||
fetch(googleUrl, { headers: { Accept: "application/json" } }),
|
||||
fetch(openLibraryUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Acad-IA info.ingenieria.lci@gmail.com",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!googleResp.ok) {
|
||||
const text = await googleResp.text().catch(() => "");
|
||||
throw new HttpError(
|
||||
502,
|
||||
"Error al consultar Google Books.",
|
||||
"GOOGLE_BOOKS_REQUEST_FAILED",
|
||||
{
|
||||
status: googleResp.status,
|
||||
statusText: googleResp.statusText,
|
||||
body: text || null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!openLibraryResp.ok) {
|
||||
const text = await openLibraryResp.text().catch(() => "");
|
||||
throw new HttpError(
|
||||
502,
|
||||
"Error al consultar Open Library.",
|
||||
"OPEN_LIBRARY_REQUEST_FAILED",
|
||||
{
|
||||
status: openLibraryResp.status,
|
||||
statusText: openLibraryResp.statusText,
|
||||
body: text || null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const googleData =
|
||||
(await googleResp.json()) as GoogleBooksVolumesListResponse;
|
||||
const openLibraryData =
|
||||
(await openLibraryResp.json()) as OpenLibrarySearchResponse;
|
||||
|
||||
const googleItems = Array.isArray(googleData?.items)
|
||||
? googleData.items.slice(0, 20)
|
||||
: [];
|
||||
const openLibraryDocs = Array.isArray(openLibraryData?.docs)
|
||||
? openLibraryData.docs.slice(0, 20)
|
||||
: [];
|
||||
|
||||
const results: EndpointResult[] = [
|
||||
...googleItems.map((item) => ({ endpoint: "google", item } as const)),
|
||||
...openLibraryDocs.map((
|
||||
item,
|
||||
) => ({ endpoint: "open_library", item } as const)),
|
||||
];
|
||||
|
||||
return sendSuccess(results);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
console.error(
|
||||
`[${new Date().toISOString()}][${functionName}] ⚠️ Handled Error:`,
|
||||
{
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
internalDetails: error.internalDetails || "N/A",
|
||||
},
|
||||
);
|
||||
return sendError(error.status, error.message, error.code);
|
||||
}
|
||||
|
||||
const unexpectedError = error instanceof Error
|
||||
? error
|
||||
: new Error(String(error));
|
||||
|
||||
console.error(
|
||||
`[${
|
||||
new Date().toISOString()
|
||||
}][${functionName}] 💥 CRITICAL UNHANDLED ERROR:`,
|
||||
unexpectedError.stack || unexpectedError.message,
|
||||
);
|
||||
|
||||
return sendError(
|
||||
500,
|
||||
"Ocurrió un error inesperado en el servidor.",
|
||||
"INTERNAL_SERVER_ERROR",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/* To invoke locally:
|
||||
|
||||
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
|
||||
2. Make an HTTP request:
|
||||
|
||||
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/buscar-bibliografia' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"name":"Functions"}'
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,100 @@
|
||||
// ./plan_mensajes_ia/index.ts
|
||||
import type { OpenAI } from "openai";
|
||||
|
||||
import type { Json } from "../../_shared/database.types.ts";
|
||||
import { ResponseMetadata } from "../../_shared/utils.ts";
|
||||
import { supabase } from "../../openai-webhook-responses/supabase.ts";
|
||||
|
||||
function extractOutputText(response: OpenAI.Responses.Response): string {
|
||||
const direct = (response as any).output_text;
|
||||
if (typeof direct === "string") return direct;
|
||||
|
||||
const output = (response as any).output;
|
||||
if (!Array.isArray(output)) return "";
|
||||
|
||||
try {
|
||||
return output
|
||||
.filter((item) => item?.type === "message")
|
||||
.flatMap((item) => item?.content ?? [])
|
||||
.filter((c) => c?.type === "output_text")
|
||||
.map((c) => String(c?.text ?? ""))
|
||||
.join("");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleAsignaturaMensajesResponse(
|
||||
response: OpenAI.Responses.Response,
|
||||
): Promise<void> {
|
||||
const metadata = response.metadata as any;
|
||||
const mensajeId = metadata?.mensaje_id;
|
||||
|
||||
console.log("Procesando Webhook para Asignatura. Mensaje ID:", mensajeId);
|
||||
|
||||
const isStructured = metadata?.is_structured === "true" || metadata?.is_structured === true;
|
||||
|
||||
if (!mensajeId) {
|
||||
console.warn("No se recibió mensaje_id en la metadata del webhook de asignatura");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const outputText = extractOutputText(response);
|
||||
if (!outputText) {
|
||||
throw new Error("La respuesta de OpenAI está vacía");
|
||||
}
|
||||
|
||||
let respuestaJSON: any;
|
||||
try {
|
||||
respuestaJSON = JSON.parse(outputText);
|
||||
} catch (e) {
|
||||
throw new Error(`Error parseando JSON de OpenAI: ${e.message}`);
|
||||
}
|
||||
|
||||
// Normalización de campos de la IA
|
||||
const aiMessage = respuestaJSON["ai-message"] || respuestaJSON["ai_message"] || "";
|
||||
const is_refusal = !!respuestaJSON.is_refusal || respuestaJSON["is-refusal"] === true;
|
||||
|
||||
let recommendations: any[] = [];
|
||||
|
||||
// Si es estructurado y no es un rechazo de la IA, generamos las recomendaciones
|
||||
if (isStructured && !is_refusal) {
|
||||
recommendations = Object.entries(respuestaJSON)
|
||||
.filter(([k]) => !["ai-message", "ai_message", "is-refusal", "is_refusal"].includes(k))
|
||||
.map(([campo, valor]) => ({
|
||||
campo_afectado: campo,
|
||||
texto_mejora: valor,
|
||||
aplicada: false,
|
||||
}));
|
||||
}
|
||||
|
||||
// --- CAMBIO CLAVE: TABLA 'asignatura_mensajes_ia' ---
|
||||
const { error } = await supabase
|
||||
.from("asignatura_mensajes_ia")
|
||||
.update({
|
||||
respuesta: aiMessage,
|
||||
// Guardamos la propuesta completa para mantener historial
|
||||
propuesta: {
|
||||
respuesta: aiMessage,
|
||||
recommendations
|
||||
},
|
||||
is_refusal,
|
||||
estado: "COMPLETADO",
|
||||
})
|
||||
.eq("id", mensajeId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
console.log(`Mensaje de asignatura ${mensajeId} actualizado con éxito.`);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error en handleAsignaturaMensajesResponse:", { mensajeId, error: e.message });
|
||||
|
||||
// Marcamos como error en la tabla correcta para que el front deje de mostrar el spinner
|
||||
await supabase
|
||||
.from("asignatura_mensajes_ia")
|
||||
.update({ estado: "ERROR" })
|
||||
.eq("id", mensajeId);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import { corsHeaders, withCors } from "./lib/cors.ts";
|
||||
import { HttpError, jsonResponse } from "./lib/errors.ts";
|
||||
import { getOpenAI } from "./lib/openai.ts";
|
||||
import { getSupabaseServiceClient, requireUser } from "./lib/supabase.ts";
|
||||
import { assertUuid, pickSchemaFields, safePlanForPrompt } from "./lib/plan.ts";
|
||||
import { assertUuid, pickSchemaFields, safePlanForPrompt,pickSchemaAsignaturaFields } from "./lib/plan.ts";
|
||||
import { OpenAIService } from "../_shared/openai-service.ts";
|
||||
|
||||
type CreateBody = {
|
||||
plan_estudio_id: string;
|
||||
@@ -22,6 +23,13 @@ type AddMessageBody = {
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
addEventListener("beforeunload", (ev: any) => {
|
||||
console.error(
|
||||
"ALERTA: La función se va a apagar. Razón:",
|
||||
ev?.detail?.reason,
|
||||
);
|
||||
});
|
||||
|
||||
// Preflight CORS
|
||||
app.options(
|
||||
"*",
|
||||
@@ -29,6 +37,13 @@ app.options(
|
||||
);
|
||||
|
||||
const prefix = "/create-chat-conversation";
|
||||
// Model names (module-level) — pueden ser sobrescritos por variables de entorno
|
||||
const CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO = Deno.env.get(
|
||||
"CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO",
|
||||
) ?? "gpt-5-nano";
|
||||
const CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO = Deno.env.get(
|
||||
"CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO",
|
||||
) ?? "gpt-5-nano";
|
||||
|
||||
app.get(`${prefix}/health`, (c) => withCors(jsonResponse({ ok: true })));
|
||||
|
||||
@@ -36,7 +51,7 @@ app.get(`${prefix}/health`, (c) => withCors(jsonResponse({ ok: true })));
|
||||
* POST /conversations
|
||||
* Crea conversación OpenAI + registro en conversaciones_plan
|
||||
*/
|
||||
app.post(`${prefix}/conversations`, async (c) => {
|
||||
app.post(`${prefix}/plan/conversations`, async (c) => {
|
||||
try {
|
||||
/* const auth = c.req.header("authorization");
|
||||
const user = await requireUser(auth); */
|
||||
@@ -108,292 +123,268 @@ app.post(`${prefix}/conversations`, async (c) => {
|
||||
return withCors(handleErr(err));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /conversations/:conversation_plan_id/messages
|
||||
* Lista mensajes (assistant/user) desde OpenAI
|
||||
*/
|
||||
app.get(`${prefix}/conversations/:id/messages`, async (c) => {
|
||||
app.post(`${prefix}/asignatura/conversations`, async (c) => {
|
||||
try {
|
||||
/* const auth = c.req.header("authorization");
|
||||
await requireUser(auth); */
|
||||
const body = (await c.req.json().catch(() => ({}))) as Partial<CreateBody>;
|
||||
const asignatura_id = body.asignatura_id;
|
||||
assertUuid(asignatura_id ?? "", "asignatura_id");
|
||||
|
||||
const conversation_plan_id = c.req.param("id");
|
||||
assertUuid(conversation_plan_id, "conversation_plan_id");
|
||||
const instanciador = body.instanciador ?? "unknown";
|
||||
const system_prompt = body.system_prompt ??
|
||||
"Eres un asistente experto en currículo académico. Si te piden algo ajeno a la asignatura, responde con un refusal.";
|
||||
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const openai = getOpenAI();
|
||||
|
||||
const { data: convRow, error } = await supabase
|
||||
.from("conversaciones_plan")
|
||||
.select("openai_conversation_id, estado")
|
||||
.eq("id", conversation_plan_id)
|
||||
// 1. Verificar que la asignatura existe
|
||||
const { data: asignatura, error: asigErr } = await supabase
|
||||
.from("asignaturas")
|
||||
.select("*")
|
||||
.eq("id", asignatura_id)
|
||||
.single();
|
||||
|
||||
if (error || !convRow) {
|
||||
throw new HttpError(
|
||||
404,
|
||||
"conversation_not_found",
|
||||
"Conversación no encontrada",
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (convRow.estado === "ARCHIVADA") {
|
||||
// si ya está archivada, devolvemos lo guardado
|
||||
const { data: archived } = await supabase
|
||||
.from("conversaciones_plan")
|
||||
.select("conversacion_json")
|
||||
.eq("id", conversation_plan_id)
|
||||
.single();
|
||||
return withCors(
|
||||
jsonResponse({
|
||||
source: "supabase",
|
||||
items: archived?.conversacion_json ?? null,
|
||||
}),
|
||||
);
|
||||
if (asigErr || !asignatura) {
|
||||
throw new HttpError(404, "asignatura_not_found", "Asignatura no encontrada");
|
||||
}
|
||||
|
||||
const items = await openai.conversations.items.list(
|
||||
convRow.openai_conversation_id,
|
||||
);
|
||||
// 2. Crear conversación en OpenAI
|
||||
const conv = await openai.conversations.create({
|
||||
metadata: {
|
||||
tabla: "asignaturas",
|
||||
id: asignatura.id,
|
||||
instanciador,
|
||||
},
|
||||
items: [{ type: "message", role: "system", content: system_prompt }],
|
||||
});
|
||||
|
||||
const conversacion = items.data.filter((it: any) =>
|
||||
it.type === "message" && (it.role === "assistant" || it.role === "user")
|
||||
).map((it: any) => ({
|
||||
role: it.role,
|
||||
content: it.content.map((c: any) => c.text).join(""),
|
||||
}));
|
||||
// 3. Insertar en conversaciones_asignatura (coincidiendo con tu SQL)
|
||||
const { data: row, error: insErr } = await supabase
|
||||
.from("conversaciones_asignatura")
|
||||
.insert({
|
||||
openai_conversation_id: conv.id,
|
||||
asignatura_id: asignatura.id,
|
||||
estado: "ACTIVA",
|
||||
conversacion_json: [], // Inicializamos como array vacío para los mensajes
|
||||
// creado_por: user.id // Opcional si tienes el ID del usuario
|
||||
})
|
||||
.select("id, asignatura_id, openai_conversation_id, estado")
|
||||
.single();
|
||||
|
||||
return withCors(jsonResponse({ source: "openai", items: conversacion }));
|
||||
if (insErr || !row) {
|
||||
try { await openai.conversations.delete(conv.id); } catch (_) {}
|
||||
throw new HttpError(500, "db_insert_failed", "Error al registrar conversación", insErr);
|
||||
}
|
||||
|
||||
return withCors(jsonResponse({ conversation_asignatura: row }, 201));
|
||||
} catch (err) {
|
||||
return withCors(handleErr(err));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* POST /conversations/:conversation_plan_id/messages
|
||||
* Agrega mensaje y opcionalmente solicita respuesta estructurada (json_schema)
|
||||
*/
|
||||
app.post(`${prefix}/conversations/:id/messages`, async (c) => {
|
||||
app.post(`${prefix}/conversations/plan/:id/messages`, async (c) => {
|
||||
try {
|
||||
/* const auth = c.req.header("authorization");
|
||||
const user = await requireUser(auth); */
|
||||
|
||||
const conversation_plan_id = c.req.param("id");
|
||||
assertUuid(conversation_plan_id, "conversation_plan_id");
|
||||
|
||||
const body = (await c.req.json().catch(() => ({}))) as Partial<
|
||||
AddMessageBody
|
||||
>;
|
||||
const body = (await c.req.json().catch(() => ({}))) as Partial<AddMessageBody>;
|
||||
if (!body.content || typeof body.content !== "string") {
|
||||
throw new HttpError(400, "bad_input", "content es requerido");
|
||||
}
|
||||
|
||||
console.log("Iniciando generación en background para mensaje_id:");
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const svc = OpenAIService.fromEnv();
|
||||
|
||||
// 1. Validar existencia y estado de la conversación
|
||||
const { data: row, error } = await supabase
|
||||
.from("conversaciones_plan")
|
||||
.select("id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))")
|
||||
.eq("id", conversation_plan_id)
|
||||
.single();
|
||||
|
||||
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
|
||||
if (row.estado === "ARCHIVADA") throw new HttpError(409, "archived", "Conversación archivada");
|
||||
|
||||
const plan = (row as any).planes_estudio;
|
||||
const definicion = plan?.estructuras_plan?.definicion;
|
||||
const isStructured = !!definicion;
|
||||
|
||||
// 2. Insertar el mensaje en estado PENDIENTE
|
||||
// Guardamos los metadatos necesarios para procesar la respuesta después
|
||||
const { data: mensajeInsertado, error: insertErr } = await supabase
|
||||
.from("plan_mensajes_ia")
|
||||
.insert({
|
||||
conversacion_plan_id:conversation_plan_id,
|
||||
enviado_por: "00000000-0000-0000-0000-000000000000",
|
||||
mensaje: body.content,
|
||||
campos: body.campos ?? [],
|
||||
estado: "PROCESANDO", // Estado inicial
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
|
||||
|
||||
// 3. Preparar Schema y Prompt
|
||||
const schema = isStructured ? pickSchemaFields(definicion, body.campos ?? []) : {
|
||||
type: "object",
|
||||
properties: { "ai-message": { type: "string" }, "is_refusal": { type: "boolean" } }
|
||||
};
|
||||
|
||||
// 4. Llamada asincrónica a OpenAI con Webhook
|
||||
// Nota: El SDK de OpenAI permite pasar webhooks en ciertos modelos/endpoints
|
||||
console.log("mandando a openaai ");
|
||||
|
||||
const aiResult = await svc.createStructuredResponse({
|
||||
conversation: row.openai_conversation_id,
|
||||
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
|
||||
background: true, // <--- ESTO ES LO QUE TE FALTABA
|
||||
metadata: {
|
||||
tabla: "plan_mensajes_ia",
|
||||
mensaje_id: String(mensajeInsertado.id), // Siempre string
|
||||
is_structured: String(isStructured)
|
||||
},
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: "definicion",
|
||||
schema: schema
|
||||
}
|
||||
},
|
||||
input: [
|
||||
{ role: "system", content: `Asistente de plan: ${JSON.stringify(safePlanForPrompt(plan))}` },
|
||||
{ role: "user", content: body.content },
|
||||
],
|
||||
});
|
||||
|
||||
if (!aiResult.ok) {
|
||||
throw new HttpError(500, "openai_error", "No se pudo encolar la respuesta");
|
||||
}
|
||||
|
||||
// 5. Responder al cliente de inmediato
|
||||
return withCors(jsonResponse({
|
||||
ok: true,
|
||||
mensaje_id: mensajeInsertado.id,
|
||||
openai_response_id: aiResult.responseId // Para seguimiento
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
return withCors(handleErr(err));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
|
||||
try {
|
||||
const conversation_asig_id = c.req.param("id");
|
||||
assertUuid(conversation_asig_id, "conversation_asig_id");
|
||||
|
||||
const body = (await c.req.json().catch(() => ({}))) as Partial<AddMessageBody>;
|
||||
if (!body.content || typeof body.content !== "string") {
|
||||
throw new HttpError(400, "bad_input", "content es requerido");
|
||||
}
|
||||
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const openai = getOpenAI();
|
||||
// Usamos el servicio que ya tienes configurado para background
|
||||
const svc = OpenAIService.fromEnv();
|
||||
|
||||
// Traer conversacion + plan + estructura
|
||||
// 1. Traer datos de la asignatura
|
||||
const { data: row, error } = await supabase
|
||||
.from("conversaciones_plan")
|
||||
.select(
|
||||
"id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))",
|
||||
)
|
||||
.eq("id", conversation_plan_id)
|
||||
.from("conversaciones_asignatura")
|
||||
.select(`id, openai_conversation_id, asignatura_id, asignaturas(*, estructuras_asignatura(definicion))`)
|
||||
.eq("id", conversation_asig_id)
|
||||
.single();
|
||||
|
||||
if (error || !row) {
|
||||
throw new HttpError(
|
||||
404,
|
||||
"conversation_not_found",
|
||||
"Conversación no encontrada",
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (row.estado === "ARCHIVADA") {
|
||||
throw new HttpError(
|
||||
409,
|
||||
"already_archived",
|
||||
"La conversación ya está archivada",
|
||||
);
|
||||
}
|
||||
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
|
||||
|
||||
const plan = (row as any).planes_estudio;
|
||||
const definicion = plan?.estructuras_plan?.definicion;
|
||||
const asignatura = (row as any).asignaturas;
|
||||
const definicion = asignatura?.estructuras_asignatura?.definicion;
|
||||
const isStructured = !!definicion && (body.campos?.length ?? 0) > 0;
|
||||
|
||||
// Si NO hay schema o no piden campos: solo agregamos mensaje y regresamos ok
|
||||
const wantsStructured = !!definicion;
|
||||
// 2. Insertar el mensaje en estado PROCESANDO (para que el front vea el spinner)
|
||||
const { data: mensajeInsertado, error: insertErr } = await supabase
|
||||
.from("asignatura_mensajes_ia")
|
||||
.insert({
|
||||
conversacion_asignatura_id: conversation_asig_id,
|
||||
enviado_por: "00000000-0000-0000-0000-000000000000",
|
||||
mensaje: body.content,
|
||||
campos: body.campos ?? [],
|
||||
estado: "PROCESANDO",
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (!wantsStructured) {
|
||||
await openai.responses.create({
|
||||
conversation: row.openai_conversation_id,
|
||||
model: "gpt-5-nano",
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content: `Este es el plan de estudios actual ${
|
||||
JSON.stringify(plan)
|
||||
}. Si te hacen una pregunta que no tiene nada que ver con el plan de estudio, responde con un refusal.`,
|
||||
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
|
||||
|
||||
// 3. Preparar Schema (Usando tu lógica de asignatura)
|
||||
const schema = isStructured
|
||||
? pickSchemaAsignaturaFields(definicion, body.campos ?? [])
|
||||
: {
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": { type: "string" },
|
||||
"is_refusal": { type: "boolean" }
|
||||
},
|
||||
{ role: "user", content: body.content },
|
||||
],
|
||||
metadata: {
|
||||
usuario: /* user.email ?? user.id ??*/ "unknown",
|
||||
plan_estudio_id: row.plan_estudio_id,
|
||||
},
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: "definicion",
|
||||
schema: {
|
||||
// Si no hay schema, igual podemos pedir mejoras estructuradas en un campo libre, pero sin validación estricta
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": {
|
||||
type: "string",
|
||||
description:
|
||||
"Mensaje de la IA para el usuario final basado en la solicitud",
|
||||
examples: [
|
||||
"Excelente, actualmente tu plan de estudio tiene una redacción clara, pero podrías mejorar el perfil de ingreso para hacerlo más atractivo.",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
required: ["ai-message", "is_refusal"],
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
return withCors(jsonResponse({ ok: true }));
|
||||
}
|
||||
|
||||
// Pedimos respuesta estructurada con responses.create
|
||||
const schema = pickSchemaFields(definicion, body.campos);
|
||||
const planForPrompt = safePlanForPrompt(plan);
|
||||
|
||||
const model = "gpt-5-nano";
|
||||
const prompt = body.user_prompt ?? body.content;
|
||||
|
||||
const resp = await openai.responses.create({
|
||||
// 4. Llamada asincrónica con background: true
|
||||
const aiResult = await svc.createStructuredResponse({
|
||||
conversation: row.openai_conversation_id,
|
||||
model,
|
||||
text: { format: { type: "json_schema", name: "definicion", schema } },
|
||||
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
|
||||
background: true, // <--- Ahora sí, activamos el modo background
|
||||
metadata: {
|
||||
usuario: /* user.email ?? user.id ??*/ "unknown",
|
||||
plan_estudio_id: row.plan_estudio_id,
|
||||
tabla: "asignatura_mensajes_ia", // El webhook usará esto para saber dónde hacer el UPDATE
|
||||
mensaje_id: String(mensajeInsertado.id),
|
||||
is_structured: String(isStructured),
|
||||
conversation_id: conversation_asig_id // Extra para el webhook si lo necesita
|
||||
},
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: isStructured ? "mejora_asignatura" : "respuesta_basica",
|
||||
schema: schema
|
||||
}
|
||||
},
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
`Eres un asistente que ayuda a mejorar este plan de estudio: ${
|
||||
JSON.stringify(planForPrompt)
|
||||
}. ` +
|
||||
`Si te hacen una pregunta que no tiene nada que ver con el plan de estudio, responde con un refusal.`,
|
||||
content: isStructured
|
||||
? `Asistente de asignatura. Datos: ${JSON.stringify(asignatura)}`
|
||||
: "Eres un experto en diseño curricular."
|
||||
},
|
||||
{ role: "user", content: prompt },
|
||||
{ role: "user", content: body.content },
|
||||
],
|
||||
});
|
||||
|
||||
let parsed: any = null;
|
||||
try {
|
||||
parsed = JSON.parse(resp.output_text ?? "null");
|
||||
} catch (_) {}
|
||||
if (!aiResult.ok) {
|
||||
throw new HttpError(500, "openai_error", "No se pudo encolar la respuesta");
|
||||
}
|
||||
|
||||
// 5. Responder al cliente de inmediato
|
||||
return withCors(jsonResponse({
|
||||
ok: true,
|
||||
openai_response_id: resp.id,
|
||||
data: parsed,
|
||||
raw: resp.output_text ?? null,
|
||||
mensaje_id: mensajeInsertado.id,
|
||||
openai_response_id: aiResult.responseId
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
return withCors(handleErr(err));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /conversations/:conversation_plan_id/archive
|
||||
* Guarda items en Supabase y elimina la conversación de OpenAI
|
||||
*/
|
||||
app.delete(`${prefix}/conversations/:id/archive`, async (c) => {
|
||||
try {
|
||||
/* const auth = c.req.header("authorization");
|
||||
await requireUser(auth); */
|
||||
|
||||
const conversation_plan_id = c.req.param("id");
|
||||
assertUuid(conversation_plan_id, "conversation_plan_id");
|
||||
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const openai = getOpenAI();
|
||||
|
||||
const { data: row, error } = await supabase
|
||||
.from("conversaciones_plan")
|
||||
.select("id, openai_conversation_id, estado")
|
||||
.eq("id", conversation_plan_id)
|
||||
.single();
|
||||
|
||||
if (error || !row) {
|
||||
throw new HttpError(
|
||||
404,
|
||||
"conversation_not_found",
|
||||
"Conversación no encontrada",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (row.estado === "ARCHIVADA") {
|
||||
return withCors(jsonResponse({ ok: true, already: true }));
|
||||
}
|
||||
|
||||
// Marcar estado
|
||||
await supabase.from("conversaciones_plan")
|
||||
.update({ estado: "ARCHIVANDO" })
|
||||
.eq("id", conversation_plan_id);
|
||||
|
||||
// Descargar items de OpenAI
|
||||
const items = await openai.conversations.items.list(
|
||||
row.openai_conversation_id,
|
||||
);
|
||||
|
||||
const conversacion = items.data.filter((it: any) =>
|
||||
it.type === "message" && (it.role === "assistant" || it.role === "user")
|
||||
).map((it: any) => ({
|
||||
role: it.role,
|
||||
content: it.content.map((c: any) => c.text).join(""),
|
||||
}));
|
||||
|
||||
// Guardar y marcar como ARCHIVADA
|
||||
const { error: upErr } = await supabase.from("conversaciones_plan")
|
||||
.update({
|
||||
estado: "ARCHIVADA",
|
||||
conversacion_json: conversacion,
|
||||
})
|
||||
.eq("id", conversation_plan_id);
|
||||
|
||||
if (upErr) {
|
||||
throw new HttpError(
|
||||
500,
|
||||
"archive_save_failed",
|
||||
"No se pudo guardar el archivo en Supabase",
|
||||
upErr,
|
||||
);
|
||||
}
|
||||
|
||||
// Borrar conversación en OpenAI (best effort)
|
||||
try {
|
||||
await openai.conversations.delete(row.openai_conversation_id);
|
||||
} catch (delErr) {
|
||||
// Queda archivada en Supabase, pero reportamos warning
|
||||
return withCors(jsonResponse({
|
||||
ok: true,
|
||||
warning: "Archivada en Supabase, pero no se pudo borrar en OpenAI",
|
||||
details: String(delErr),
|
||||
}, 200));
|
||||
}
|
||||
|
||||
return withCors(jsonResponse({ ok: true }));
|
||||
} catch (err) {
|
||||
return withCors(handleErr(err));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Unknown routes
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const corsHeaders: Record<string, string> = {
|
||||
"access-control-allow-origin": "*",
|
||||
"access-control-allow-headers":
|
||||
"authorization, x-client-info, apikey, content-type",
|
||||
"access-control-allow-methods": "GET,POST,OPTIONS",
|
||||
"Access-Control-Allow-Headers":
|
||||
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform",
|
||||
"access-control-allow-methods": "GET,POST,OPTIONS,DELETE",
|
||||
};
|
||||
|
||||
export function withCors(res: Response) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HttpError } from "./errors.ts";
|
||||
|
||||
export function pickSchemaFields(
|
||||
definicion: any,
|
||||
campos?: string[],
|
||||
campos: string[],
|
||||
) {
|
||||
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
||||
return definicion;
|
||||
@@ -18,20 +18,23 @@ export function pickSchemaFields(
|
||||
"Listo: mejoré la redacción del perfil de ingreso y propuse un tema de investigación alineado al plan.",
|
||||
],
|
||||
},
|
||||
"is-refusal": {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Indica si el plan fue rechazado por el modelo. En caso de ser true, se espera un mensaje de rechazo en `ai-message`.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let out = structuredClone(definicion);
|
||||
const out = structuredClone(definicion);
|
||||
|
||||
// Si piden campos, filtramos propiedades/required a esos campos
|
||||
if (Array.isArray(campos) && campos.length > 0) {
|
||||
const entries = Object.entries(out.properties).filter(([k]) =>
|
||||
campos.includes(k)
|
||||
);
|
||||
out.properties = Object.fromEntries(entries);
|
||||
if (Array.isArray(out.required)) {
|
||||
out.required = out.required.filter((k: string) => campos.includes(k));
|
||||
}
|
||||
const entries = Object.entries(out.properties).filter(([k]) =>
|
||||
campos.includes(k)
|
||||
);
|
||||
out.properties = Object.fromEntries(entries);
|
||||
if (Array.isArray(out.required)) {
|
||||
out.required = out.required.filter((k: string) => campos.includes(k));
|
||||
}
|
||||
|
||||
// Siempre agregamos ai-message
|
||||
@@ -42,6 +45,49 @@ export function pickSchemaFields(
|
||||
|
||||
return out;
|
||||
}
|
||||
export function pickSchemaAsignaturaFields(
|
||||
definicion: any,
|
||||
campos: string[],
|
||||
) {
|
||||
// 1. Validación inicial
|
||||
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": { type: "string" },
|
||||
"is_refusal": { type: "boolean" }
|
||||
},
|
||||
required: ["ai-message", "is_refusal"],
|
||||
additionalProperties: false
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Filtrar solo las propiedades técnicas que el usuario pidió
|
||||
const filteredProperties = Object.fromEntries(
|
||||
Object.entries(definicion.properties).filter(([k]) => campos.includes(k))
|
||||
);
|
||||
|
||||
// 3. RECONSTRUIR el esquema incluyendo SIEMPRE los campos de control
|
||||
const finalSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"ai-message": {
|
||||
type: "string",
|
||||
description: "Tu respuesta conversacional dirigida al profesor."
|
||||
},
|
||||
"is_refusal": {
|
||||
type: "boolean",
|
||||
description: "Indica si la solicitud es inapropiada o no relacionada."
|
||||
},
|
||||
...filteredProperties // Aquí entran objetivo, contenido, etc.
|
||||
},
|
||||
// Forzamos que ai-message e is_refusal sean obligatorios siempre
|
||||
required: ["ai-message", "is_refusal", ...Object.keys(filteredProperties)],
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
return finalSchema;
|
||||
}
|
||||
|
||||
export function safePlanForPrompt(plan: any) {
|
||||
const copy = structuredClone(plan);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// ./plan_mensajes_ia/index.ts
|
||||
import type { OpenAI } from "openai";
|
||||
|
||||
import type { Json } from "../../_shared/database.types.ts";
|
||||
import { ResponseMetadata } from "../../_shared/utils.ts";
|
||||
import { supabase } from "../../openai-webhook-responses/supabase.ts";
|
||||
|
||||
|
||||
function extractOutputText(response: OpenAI.Responses.Response): string {
|
||||
const direct = (response as unknown as { output_text?: unknown }).output_text;
|
||||
if (typeof direct === "string") return direct;
|
||||
|
||||
const output = (response as unknown as { output?: unknown }).output;
|
||||
if (!Array.isArray(output)) return "";
|
||||
|
||||
// Fallback similar al usado en index.ts
|
||||
try {
|
||||
return output
|
||||
.filter((item) => (item as { type?: unknown })?.type === "message")
|
||||
.flatMap((item) => (item as { content?: unknown })?.content ?? [])
|
||||
.filter((c) => (c as { type?: unknown })?.type === "output_text")
|
||||
.map((c) => String((c as { text?: unknown })?.text ?? ""))
|
||||
.join("");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePlanMensajesResponse(
|
||||
response: OpenAI.Responses.Response,
|
||||
): Promise<void> {
|
||||
const metadata = response.metadata as any;
|
||||
const mensajeId = metadata?.mensaje_id;
|
||||
console.log("ya entre aqui");
|
||||
|
||||
const isStructured = metadata?.is_structured === "true" || metadata?.is_structured === true;
|
||||
if (!mensajeId) {
|
||||
console.warn("No se recibió mensaje_id en la metadata del webhook");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const outputText = extractOutputText(response);
|
||||
if (!outputText) {
|
||||
throw new Error("La respuesta de OpenAI está vacía");
|
||||
}
|
||||
|
||||
let respuestaJSON: any;
|
||||
try {
|
||||
respuestaJSON = JSON.parse(outputText);
|
||||
} catch (e) {
|
||||
throw new Error(`Error parseando JSON de OpenAI: ${e.message}`);
|
||||
}
|
||||
|
||||
const is_refusal = !!respuestaJSON.is_refusal || respuestaJSON["is-refusal"] === true;
|
||||
|
||||
let recommendations = [];
|
||||
if (isStructured && !is_refusal) {
|
||||
recommendations = Object.entries(respuestaJSON)
|
||||
.filter(([k]) => k !== "ai-message" && k !== "is-refusal" && k !== "is_refusal")
|
||||
.map(([campo, valor]) => ({
|
||||
campo_afectado: campo,
|
||||
texto_mejora: valor,
|
||||
aplicada: false,
|
||||
}));
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("plan_mensajes_ia")
|
||||
.update({
|
||||
respuesta: respuestaJSON["ai-message"] || "",
|
||||
propuesta: { recommendations },
|
||||
is_refusal,
|
||||
estado: "COMPLETADO",
|
||||
})
|
||||
.eq("id", mensajeId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error procesando handlePlanMensajesResponse:", { mensajeId, e });
|
||||
// Opcional: Marcar el mensaje como fallido en la tabla si tienes ese estado
|
||||
await supabase
|
||||
.from("plan_mensajes_ia")
|
||||
.update({ estado: "ERROR" })
|
||||
.eq("id", mensajeId);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ import {
|
||||
type StructuredResponseOptions,
|
||||
} from "../_shared/openai-service.ts";
|
||||
|
||||
addEventListener("beforeunload", (ev: any) => {
|
||||
console.error(
|
||||
"ALERTA: La función se va a apagar. Razón:",
|
||||
ev?.detail?.reason,
|
||||
);
|
||||
});
|
||||
|
||||
export type DataAsignaturaSugerida = {
|
||||
nombre: Tables<"asignaturas">["nombre"];
|
||||
codigo?: Tables<"asignaturas">["codigo"];
|
||||
@@ -160,6 +167,11 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
SERVICE_ROLE_KEY,
|
||||
);
|
||||
|
||||
// Model name controlled via env var
|
||||
const GENERATE_SUBJECT_SUGGESTIONS_MODELO = Deno.env.get(
|
||||
"GENERATE_SUBJECT_SUGGESTIONS_MODELO",
|
||||
) ?? "gpt-5-mini";
|
||||
|
||||
const { data: plan, error: planError } = await supabaseService
|
||||
.from("planes_estudio")
|
||||
.select("id,nombre,nivel,tipo_ciclo,numero_ciclos,datos")
|
||||
@@ -276,7 +288,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
}
|
||||
|
||||
const options: StructuredResponseOptions = {
|
||||
model: "gpt-5-mini",
|
||||
model: GENERATE_SUBJECT_SUGGESTIONS_MODELO,
|
||||
input: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// ...existing code...
|
||||
import type { DataAsignaturaSugerida } from "./index.ts";
|
||||
|
||||
export const sugerencias: DataAsignaturaSugerida[][] = [
|
||||
[
|
||||
{
|
||||
nombre: "Transferencia de Calor",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 5,
|
||||
descripcion:
|
||||
"Conducción, convección y radiación; diseño de aletas e intercambiadores; análisis transitorio y numérico en equipos térmicos.",
|
||||
horasAcademicas: 43,
|
||||
horasIndependientes: 35,
|
||||
},
|
||||
{
|
||||
nombre: "Mecánica de Fluidos II",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 4.5,
|
||||
descripcion:
|
||||
"Flujo turbulento, capas límite, turbomáquinas y redes de tuberías; medición y control de caudal; CFD básico para aplicaciones mecánicas.",
|
||||
horasAcademicas: 43,
|
||||
horasIndependientes: 34,
|
||||
},
|
||||
{
|
||||
nombre: "Diseño de Elementos de Máquinas I",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 5,
|
||||
descripcion:
|
||||
"Selección y dimensionamiento de ejes, tornillos, resortes, engranajes y uniones; criterios de falla estática y a fatiga; normas y materiales.",
|
||||
horasAcademicas: 43,
|
||||
horasIndependientes: 35,
|
||||
},
|
||||
{
|
||||
nombre: "Dinámica de Máquinas",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 4.5,
|
||||
descripcion:
|
||||
"Modelado y análisis de mecanismos, cinemática y dinámica multicuerpo; fuerzas inerciales, equilibrado, resortes y amortiguadores, trenes de leva.",
|
||||
horasAcademicas: 43,
|
||||
horasIndependientes: 34,
|
||||
},
|
||||
{
|
||||
nombre: "Vibraciones Mecánicas",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 4,
|
||||
descripcion:
|
||||
"Vibraciones libres y forzadas en 1 y múltiples grados; amortiguamiento, resonancia, aislamiento y balanceo; análisis modal y mediciones.",
|
||||
horasAcademicas: 33,
|
||||
horasIndependientes: 33,
|
||||
},
|
||||
{
|
||||
nombre: "Control Automático para Sistemas Mecánicos",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 4,
|
||||
descripcion:
|
||||
"Modelado LTI, funciones de transferencia y espacio de estados; diseño PID y de compensadores; sensores y actuadores en sistemas mecánicos.",
|
||||
horasAcademicas: 33,
|
||||
horasIndependientes: 33,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
nombre: "Estructuras de Datos y Algoritmos",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 6,
|
||||
descripcion:
|
||||
"Diseño, análisis e implementación de estructuras de datos (listas, pilas, colas, árboles, grafos, tablas hash) y algoritmos fundamentales; complejidad temporal y espacial; aplicación a resolución eficiente de problemas.",
|
||||
horasAcademicas: 60,
|
||||
horasIndependientes: 30,
|
||||
},
|
||||
{
|
||||
nombre: "Programación Orientada a Objetos",
|
||||
tipo: "OBLIGATORIA",
|
||||
creditos: 5,
|
||||
descripcion:
|
||||
"Principios y prácticas de la programación orientada a objetos: clases, objetos, herencia, polimorfismo, diseño basado en patrones, pruebas unitarias y gestión de proyectos simples en lenguajes modernos.",
|
||||
horasAcademicas: 50,
|
||||
horasIndependientes: 25,
|
||||
},
|
||||
{
|
||||
nombre: "Matemáticas Discretas",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 4,
|
||||
descripcion:
|
||||
"Conceptos fundamentales: lógica proposicional y de predicados, teoría de conjuntos, relaciones y funciones, combinatoria, teoría de grafos y demostraciones formales aplicadas a la computación.",
|
||||
horasAcademicas: 40,
|
||||
horasIndependientes: 20,
|
||||
},
|
||||
{
|
||||
nombre: "Circuitos Digitales y Lógica",
|
||||
tipo: "OBLIGATORIA",
|
||||
creditos: 4,
|
||||
descripcion:
|
||||
"Análisis y diseño de circuitos digitales: álgebra booleana, puertas lógicas, minimización, diseño secuencial y combinacional, máquinas de estados y uso de herramientas de simulación.",
|
||||
horasAcademicas: 40,
|
||||
horasIndependientes: 20,
|
||||
},
|
||||
{
|
||||
nombre: "Señales y Sistemas Básicos",
|
||||
tipo: "TRONCAL",
|
||||
creditos: 4,
|
||||
descripcion:
|
||||
"Introducción a señales y sistemas: representación de señales, sistemas lineales invariantes en el tiempo, transformadas (Fourier, Laplace) y aplicaciones básicas en procesamiento y comunicaciones.",
|
||||
horasAcademicas: 40,
|
||||
horasIndependientes: 20,
|
||||
},
|
||||
{
|
||||
nombre: "Laboratorio de Programación y Prácticas de Ingeniería",
|
||||
tipo: "OPTATIVA",
|
||||
creditos: 3,
|
||||
descripcion:
|
||||
"Asignatura práctica orientada a aplicar técnicas de programación, integración de software y herramientas de desarrollo; proyectos cortos que consolidan conceptos de algoritmos y diseño de software.",
|
||||
horasAcademicas: 30,
|
||||
horasIndependientes: 15,
|
||||
},
|
||||
],
|
||||
];
|
||||
// ...existing code...
|
||||
@@ -0,0 +1,3 @@
|
||||
# Configuration for private npm package dependencies
|
||||
# For more information on using private registries with Edge Functions, see:
|
||||
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { OpenAI } from "openai";
|
||||
import { supabase } from "../supabase.ts";
|
||||
import type { Json } from "../../_shared/database.types.ts";
|
||||
import { ResponseMetadata } from "../../_shared/utils.ts";
|
||||
|
||||
function extractOutputText(response: OpenAI.Responses.Response): string {
|
||||
const direct = (response as unknown as { output_text?: unknown }).output_text;
|
||||
if (typeof direct === "string") return direct;
|
||||
|
||||
const output = (response as unknown as { output?: unknown }).output;
|
||||
if (!Array.isArray(output)) return "";
|
||||
|
||||
try {
|
||||
return output
|
||||
.filter((item) => (item as { type?: unknown })?.type === "message")
|
||||
.flatMap((item) => (item as { content?: unknown })?.content ?? [])
|
||||
.filter((c) => (c as { type?: unknown })?.type === "output_text")
|
||||
.map((c) => String((c as { text?: unknown })?.text ?? ""))
|
||||
.join("");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function splitAiOutputStringsAndColumns(
|
||||
aiOutput: unknown,
|
||||
): { aiOutputJson: Json; columnasGeneradas: Record<string, unknown> } {
|
||||
if (!aiOutput || typeof aiOutput !== "object" || Array.isArray(aiOutput)) {
|
||||
return { aiOutputJson: aiOutput as unknown as Json, columnasGeneradas: {} };
|
||||
}
|
||||
|
||||
const record = aiOutput as Record<string, unknown>;
|
||||
const stringsOnly: Record<string, string> = {};
|
||||
const columnasGeneradas: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (typeof value === "string") {
|
||||
stringsOnly[key] = value;
|
||||
} else {
|
||||
columnasGeneradas[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { aiOutputJson: stringsOnly as unknown as Json, columnasGeneradas };
|
||||
}
|
||||
|
||||
async function marcarFalloAsignatura(
|
||||
asignaturaId: string,
|
||||
reason: string,
|
||||
extra?: unknown,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { data: existing, error: existingError } = await supabase
|
||||
.from("asignaturas")
|
||||
.select("meta_origen")
|
||||
.eq("id", asignaturaId)
|
||||
.maybeSingle();
|
||||
if (existingError) {
|
||||
console.warn("No se pudo leer meta_origen para marcar fallo", {
|
||||
asignaturaId,
|
||||
existingError,
|
||||
});
|
||||
}
|
||||
|
||||
const baseMeta = (existing?.meta_origen &&
|
||||
typeof existing.meta_origen === "object" &&
|
||||
!Array.isArray(existing.meta_origen))
|
||||
? (existing.meta_origen as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const nextMeta: Record<string, unknown> = {
|
||||
...baseMeta,
|
||||
ai_error: {
|
||||
reason,
|
||||
extra: extra ?? null,
|
||||
at: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from("asignaturas")
|
||||
.update({ estado: "borrador", meta_origen: nextMeta as unknown as Json })
|
||||
.eq("id", asignaturaId);
|
||||
if (error) {
|
||||
console.warn("No se pudo marcar fallo en asignatura", {
|
||||
asignaturaId,
|
||||
error,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Fallo inesperado marcando fallo en asignatura", {
|
||||
asignaturaId,
|
||||
e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleCrearAsignaturaResponse(
|
||||
response: OpenAI.Responses.Response,
|
||||
): Promise<void> {
|
||||
const metadata = response.metadata as ResponseMetadata | null;
|
||||
const asignaturaId = metadata?.id;
|
||||
if (!asignaturaId) {
|
||||
console.warn("No se recibió metadata.id para actualizar la asignatura");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const outputText = extractOutputText(response);
|
||||
if (!outputText) {
|
||||
console.warn("La respuesta no contiene output_text");
|
||||
await marcarFalloAsignatura(asignaturaId, "MISSING_OUTPUT_TEXT", {
|
||||
responseId: response.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let aiOutput: unknown;
|
||||
try {
|
||||
aiOutput = JSON.parse(outputText);
|
||||
} catch (e) {
|
||||
console.warn("No se pudo parsear JSON de la respuesta", e);
|
||||
await marcarFalloAsignatura(asignaturaId, "INVALID_JSON", {
|
||||
responseId: response.id,
|
||||
outputText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { aiOutputJson, columnasGeneradas } = splitAiOutputStringsAndColumns(
|
||||
aiOutput,
|
||||
);
|
||||
|
||||
const { data: existing, error: existingError } = await supabase
|
||||
.from("asignaturas")
|
||||
.select("meta_origen")
|
||||
.eq("id", asignaturaId)
|
||||
.maybeSingle();
|
||||
|
||||
if (existingError) {
|
||||
console.warn("No se pudo leer meta_origen existente", {
|
||||
asignaturaId,
|
||||
existingError,
|
||||
});
|
||||
}
|
||||
|
||||
const baseMeta = (existing?.meta_origen &&
|
||||
typeof existing.meta_origen === "object" &&
|
||||
!Array.isArray(existing.meta_origen))
|
||||
? (existing.meta_origen as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const nextMeta: Record<string, unknown> = {
|
||||
...baseMeta,
|
||||
ai: {
|
||||
...(typeof baseMeta.ai === "object" && baseMeta.ai &&
|
||||
!Array.isArray(baseMeta.ai)
|
||||
? (baseMeta.ai as Record<string, unknown>)
|
||||
: {}),
|
||||
responseId: response.id,
|
||||
model: response.model,
|
||||
},
|
||||
};
|
||||
|
||||
const updatePatch: Record<string, unknown> = {
|
||||
datos: aiOutputJson,
|
||||
estado: "borrador",
|
||||
tipo_origen: "IA",
|
||||
meta_origen: nextMeta as unknown as Json,
|
||||
};
|
||||
|
||||
for (const value of Object.values(columnasGeneradas)) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
continue;
|
||||
}
|
||||
const xColumn = (value as Record<string, unknown>)["x-column"];
|
||||
const xDef = (value as Record<string, unknown>)["x-definicion"];
|
||||
if (typeof xColumn !== "string" || !xColumn.length) continue;
|
||||
updatePatch[xColumn] = xDef as unknown as Json;
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from("asignaturas")
|
||||
.update(
|
||||
updatePatch as unknown as {
|
||||
[k: string]: unknown;
|
||||
},
|
||||
)
|
||||
.eq("id", asignaturaId);
|
||||
|
||||
if (updateError) {
|
||||
console.warn("No se pudo actualizar asignatura con datos", {
|
||||
asignaturaId,
|
||||
updateError,
|
||||
});
|
||||
await marcarFalloAsignatura(asignaturaId, "SUPABASE_UPDATE_FAILED", {
|
||||
updateError,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Fallo inesperado procesando asignatura", {
|
||||
asignaturaId,
|
||||
e,
|
||||
});
|
||||
await marcarFalloAsignatura(asignaturaId, "UNEXPECTED", { e });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { OpenAI } from "openai";
|
||||
import { ResponseMetadata } from "../../_shared/utils.ts";
|
||||
import { handleCrearAsignaturaResponse } from "./crear.ts";
|
||||
|
||||
export async function handleAsignaturasResponse(
|
||||
response: OpenAI.Responses.Response,
|
||||
): Promise<void> {
|
||||
const metadata = response.metadata as ResponseMetadata | null;
|
||||
const accion = metadata?.accion;
|
||||
|
||||
if (!accion) {
|
||||
console.warn("No se recibió metadata.accion para asignaturas");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (accion) {
|
||||
case "crear":
|
||||
case "actualizar":
|
||||
await handleCrearAsignaturaResponse(response);
|
||||
return;
|
||||
default:
|
||||
console.warn("Acción no reconocida para asignaturas:", accion);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"imports": {
|
||||
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
||||
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts",
|
||||
"express": "npm:express@5.2.1",
|
||||
"openai": "npm:openai@6.16.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||
import OpenAI from "openai";
|
||||
import { ResponseMetadata } from "../_shared/utils.ts";
|
||||
import { handlePlanesEstudioResponse } from "./planes_estudio/index.ts";
|
||||
import { handleAsignaturasResponse } from "./asignaturas/index.ts";
|
||||
import { handlePlanMensajesResponse } from "../create-chat-conversation/plan/crear.ts";
|
||||
import { handleAsignaturaMensajesResponse } from "../create-chat-conversation/asignatura/crear.ts";
|
||||
|
||||
|
||||
|
||||
console.log("Starting OpenAI webhook responses function");
|
||||
const client = new OpenAI({
|
||||
webhookSecret: Deno.env.get("OPENAI_WEBHOOK_SECRET"),
|
||||
});
|
||||
|
||||
async function handleCompletedResponse(
|
||||
event: OpenAI.Webhooks.ResponseCompletedWebhookEvent,
|
||||
) {
|
||||
const response_id = event.data.id;
|
||||
const response = await client.responses.retrieve(response_id);
|
||||
const metadata = response.metadata as ResponseMetadata | null;
|
||||
console.log(("entre"));
|
||||
|
||||
if (!metadata || !metadata.tabla) {
|
||||
console.warn("No se recibió metadata o tabla en la respuesta");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (metadata.tabla) {
|
||||
case "planes_estudio":
|
||||
await handlePlanesEstudioResponse(response);
|
||||
break;
|
||||
case "asignaturas":
|
||||
await handleAsignaturasResponse(response);
|
||||
break;
|
||||
case "plan_mensajes_ia":
|
||||
await handlePlanMensajesResponse(response);
|
||||
break;
|
||||
case "asignatura_mensajes_ia":
|
||||
console.log("modificando asignatura");
|
||||
|
||||
await handleAsignaturaMensajesResponse(response);
|
||||
break;
|
||||
default:
|
||||
console.warn("Tabla no reconocida:", metadata.tabla);
|
||||
}
|
||||
|
||||
const direct = (response as unknown as { output_text?: unknown }).output_text;
|
||||
if (typeof direct === "string" && direct.length) {
|
||||
console.log("Response output:", direct);
|
||||
return;
|
||||
}
|
||||
|
||||
const output = (response as unknown as { output?: unknown }).output;
|
||||
if (!Array.isArray(output)) {
|
||||
console.log("Response output: (no output)");
|
||||
return;
|
||||
}
|
||||
|
||||
const outputText = output
|
||||
.filter((item) => (item as { type?: unknown })?.type === "message")
|
||||
.flatMap((item) => (item as { content?: unknown })?.content ?? [])
|
||||
.filter((c) => (c as { type?: unknown })?.type === "output_text")
|
||||
.map((c) => String((c as { text?: unknown })?.text ?? ""))
|
||||
.join("");
|
||||
|
||||
console.log("Response output:", outputText);
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const url = new URL(req.url);
|
||||
const functionName = url.pathname.split("/").pop();
|
||||
console.log(
|
||||
`[${new Date().toISOString()}][${functionName}]: Request received`,
|
||||
);
|
||||
|
||||
// Opcional: Solo permitir POST
|
||||
if (req.method !== "POST") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
try {
|
||||
// OpenAI requiere el body como texto crudo (string) para verificar la firma criptográfica
|
||||
const payload = await req.text();
|
||||
|
||||
// Validar firma y parsear el evento. Deno maneja req.headers compatiblemente
|
||||
const event = await client.webhooks.unwrap(payload, req.headers);
|
||||
|
||||
switch (event.type) {
|
||||
case "response.completed": {
|
||||
EdgeRuntime.waitUntil(handleCompletedResponse(event));
|
||||
break;
|
||||
}
|
||||
case "response.cancelled": {
|
||||
console.warn("Response cancelled:", event.data);
|
||||
break;
|
||||
}
|
||||
case "response.failed": {
|
||||
console.error("Response failed:", event.data);
|
||||
break;
|
||||
}
|
||||
case "response.incomplete": {
|
||||
console.warn("Response incomplete:", event.data);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${
|
||||
new Date().toISOString()
|
||||
}][${functionName}]: Request processed successfully`,
|
||||
);
|
||||
return new Response("OK", { status: 200 });
|
||||
} catch (error) {
|
||||
if (error instanceof OpenAI.InvalidWebhookSignatureError) {
|
||||
console.error("Invalid signature:", error.message);
|
||||
return new Response("Invalid signature", { status: 400 });
|
||||
} else {
|
||||
console.error("Internal Error:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { OpenAI } from "openai";
|
||||
import { supabase } from "../supabase.ts";
|
||||
import type { Json } from "../../_shared/database.types.ts";
|
||||
import { ResponseMetadata } from "../../_shared/utils.ts";
|
||||
|
||||
async function getEstadoId(clave: string): Promise<string | null> {
|
||||
const { data, error } = await supabase
|
||||
.from("estados_plan")
|
||||
.select("id")
|
||||
.eq("clave", clave)
|
||||
.maybeSingle();
|
||||
if (error) {
|
||||
console.warn("No se pudo obtener estado:", clave, error);
|
||||
return null;
|
||||
}
|
||||
return data?.id ?? null;
|
||||
}
|
||||
|
||||
async function marcarFallido(planId: string): Promise<void> {
|
||||
const fallId = await getEstadoId("FALLDA");
|
||||
if (!fallId) {
|
||||
console.warn("No existe estado FALLDA; no se pudo marcar fallo", {
|
||||
planId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { error } = await supabase
|
||||
.from("planes_estudio")
|
||||
.update({ estado_actual_id: fallId })
|
||||
.eq("id", planId);
|
||||
if (error) {
|
||||
console.warn("No se pudo marcar plan como FALLDA", { planId, error });
|
||||
}
|
||||
}
|
||||
|
||||
function extractOutputText(response: OpenAI.Responses.Response): string {
|
||||
const direct = (response as unknown as { output_text?: unknown }).output_text;
|
||||
if (typeof direct === "string") return direct;
|
||||
|
||||
const output = (response as unknown as { output?: unknown }).output;
|
||||
if (!Array.isArray(output)) return "";
|
||||
|
||||
// Fallback similar al usado en index.ts
|
||||
try {
|
||||
return output
|
||||
.filter((item) => (item as { type?: unknown })?.type === "message")
|
||||
.flatMap((item) => (item as { content?: unknown })?.content ?? [])
|
||||
.filter((c) => (c as { type?: unknown })?.type === "output_text")
|
||||
.map((c) => String((c as { text?: unknown })?.text ?? ""))
|
||||
.join("");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleCrearPlanEstudio(
|
||||
response: OpenAI.Responses.Response,
|
||||
): Promise<void> {
|
||||
const metadata = response.metadata as ResponseMetadata | null;
|
||||
const planId = metadata?.id;
|
||||
if (!planId) {
|
||||
console.warn("No se recibió metadata.id para actualizar el plan");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const borradorId = await getEstadoId("BORRADOR");
|
||||
if (!borradorId) {
|
||||
console.warn("No existe estado BORRADOR");
|
||||
await marcarFallido(planId);
|
||||
return;
|
||||
}
|
||||
|
||||
const outputText = extractOutputText(response);
|
||||
if (!outputText) {
|
||||
console.warn("La respuesta no contiene output_text");
|
||||
await marcarFallido(planId);
|
||||
return;
|
||||
}
|
||||
|
||||
let datos: Json;
|
||||
try {
|
||||
datos = JSON.parse(outputText) as Json;
|
||||
} catch (e) {
|
||||
console.warn("No se pudo parsear JSON de la respuesta", e);
|
||||
await marcarFallido(planId);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("planes_estudio")
|
||||
.update({ datos, estado_actual_id: borradorId })
|
||||
.eq("id", planId);
|
||||
|
||||
if (error) {
|
||||
console.warn("No se pudo actualizar el plan con datos", {
|
||||
planId,
|
||||
error,
|
||||
});
|
||||
await marcarFallido(planId);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Fallo inesperado procesando plan", { planId, e });
|
||||
await marcarFallido(planId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ResponseMetadata } from "../../_shared/utils.ts";
|
||||
import type { OpenAI } from "openai";
|
||||
import { handleCrearPlanEstudio } from "./crear.ts";
|
||||
|
||||
export async function handlePlanesEstudioResponse(
|
||||
response: OpenAI.Responses.Response,
|
||||
): Promise<void> {
|
||||
const metadata = response.metadata as ResponseMetadata | null;
|
||||
if (!metadata || !metadata.accion) {
|
||||
console.warn("No se recibió acción en la metadata");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (metadata.accion) {
|
||||
case "crear":
|
||||
await handleCrearPlanEstudio(response);
|
||||
return;
|
||||
case "actualizar":
|
||||
// Lógica para actualizar un plan de estudio
|
||||
return;
|
||||
case "eliminar":
|
||||
// Lógica para eliminar un plan de estudio
|
||||
return;
|
||||
default:
|
||||
console.warn("Acción no reconocida:", metadata.accion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "../_shared/database.types.ts";
|
||||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
||||
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
||||
|
||||
if (!SUPABASE_URL || (!SUPABASE_SERVICE_ROLE_KEY && !SUPABASE_ANON_KEY)) {
|
||||
console.warn("Missing Supabase environment variables");
|
||||
}
|
||||
|
||||
const supabaseKey = (SUPABASE_SERVICE_ROLE_KEY ?? SUPABASE_ANON_KEY) as string;
|
||||
|
||||
const supabase = createClient<Database>(
|
||||
SUPABASE_URL as string,
|
||||
supabaseKey,
|
||||
);
|
||||
|
||||
export { supabase };
|
||||
@@ -141,7 +141,7 @@ Deno.test("ai-structured (multipart + file)", async () => {
|
||||
assert(typeof data.output?.resumen === "string");
|
||||
assert(data.output.resumen.length > 0);
|
||||
|
||||
// opcional: valida que registró upload(s) a storage
|
||||
assert(Array.isArray(data.references?.uploadedToStorage));
|
||||
// opcional: valida que registró upload(s) a OpenAI files
|
||||
assert(Array.isArray(data.references?.openaiFileIds));
|
||||
});
|
||||
*/
|
||||
|
||||
@@ -160,6 +160,6 @@ Deno.test({
|
||||
assert(typeof data.output?.resumen === "string");
|
||||
assert(data.output.resumen.length > 0);
|
||||
|
||||
// opcional: valida que registró upload(s) a storage
|
||||
assert(Array.isArray(data.references?.uploadedToStorage));
|
||||
// opcional: valida que registró upload(s) a OpenAI files
|
||||
assert(Array.isArray(data.references?.openaiFileIds));
|
||||
});
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped";
|
||||
|
||||
create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION');
|
||||
|
||||
alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
||||
|
||||
alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
||||
|
||||
drop type "public"."tipo_cambio__old_version_to_be_dropped";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
create or replace view "public"."plantilla_plan" as SELECT plan.id AS plan_estudio_id,
|
||||
struct.id AS estructura_id,
|
||||
struct.template_id
|
||||
FROM (public.planes_estudio plan
|
||||
JOIN public.estructuras_plan struct ON ((plan.estructura_id = struct.id)));
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.set_actualizado_en()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
begin
|
||||
new.actualizado_en = now();
|
||||
return new;
|
||||
end;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.unaccent_immutable(text)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS $function$
|
||||
SELECT public.unaccent('public.unaccent', $1);
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.validar_numero_ciclo_asignatura()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
declare
|
||||
v_numero_ciclos int;
|
||||
begin
|
||||
if new.numero_ciclo is null then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
select pe.numero_ciclos into v_numero_ciclos
|
||||
from planes_estudio pe
|
||||
where pe.id = new.plan_estudio_id;
|
||||
|
||||
if v_numero_ciclos is null then
|
||||
raise exception 'plan_estudio_id inválido %, plan no encontrado', new.plan_estudio_id;
|
||||
end if;
|
||||
|
||||
if new.numero_ciclo < 1 then
|
||||
raise exception 'numero_ciclo debe ser >= 1 (recibido %)', new.numero_ciclo;
|
||||
end if;
|
||||
|
||||
if new.numero_ciclo > v_numero_ciclos then
|
||||
raise exception 'numero_ciclo % excede planes_estudio.numero_ciclos % para plan_estudio_id %',
|
||||
new.numero_ciclo, v_numero_ciclos, new.plan_estudio_id;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$function$
|
||||
;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- alter table "public"."asignaturas" drop constraint "asignaturas_facultad_propietaria_id_fkey";
|
||||
|
||||
-- alter table "public"."asignaturas" drop constraint "asignaturas_horas_semana_check";
|
||||
|
||||
-- alter table "public"."asignaturas" drop column "facultad_propietaria_id";
|
||||
|
||||
alter table "public"."asignaturas" drop column "horas_semana";
|
||||
|
||||
alter table "public"."asignaturas" add column "horas_academicas" integer;
|
||||
|
||||
alter table "public"."asignaturas" add column "horas_independientes" integer;
|
||||
|
||||
alter table "public"."asignaturas" add constraint "asignaturas_horas_academicas_check" CHECK (((horas_academicas IS NULL) OR (horas_academicas >= 0))) not valid;
|
||||
|
||||
alter table "public"."asignaturas" validate constraint "asignaturas_horas_academicas_check";
|
||||
|
||||
alter table "public"."asignaturas" add constraint "asignaturas_horas_independientes_check" CHECK (((horas_independientes IS NULL) OR (horas_independientes >= 0))) not valid;
|
||||
|
||||
alter table "public"."asignaturas" validate constraint "asignaturas_horas_independientes_check";
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
create extension if not exists "http" with schema "extensions";
|
||||
|
||||
alter table "public"."asignaturas" drop constraint if exists "asignaturas_facultad_propietaria_id_fkey";
|
||||
|
||||
alter table "public"."cambios_plan" drop constraint "cambios_plan_plan_estudio_id_fkey";
|
||||
|
||||
alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped";
|
||||
|
||||
create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION', 'ACTUALIZACION');
|
||||
|
||||
alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
||||
|
||||
alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
||||
|
||||
drop type "public"."tipo_cambio__old_version_to_be_dropped";
|
||||
|
||||
alter table "public"."asignaturas" drop column if exists "facultad_propietaria_id";
|
||||
|
||||
alter table "public"."asignaturas" add column "asignatura_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored;
|
||||
|
||||
alter table "public"."cambios_plan" drop column "interaccion_ia_id";
|
||||
|
||||
alter table "public"."cambios_plan" add column "response_id" text;
|
||||
|
||||
alter table "public"."planes_estudio" add column "conversation_id" text;
|
||||
|
||||
alter table "public"."planes_estudio" add column "plan_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored;
|
||||
|
||||
CREATE UNIQUE INDEX planes_estudio_conversation_id_key ON public.planes_estudio USING btree (conversation_id);
|
||||
|
||||
alter table "public"."planes_estudio" add constraint "planes_estudio_conversation_id_key" UNIQUE using index "planes_estudio_conversation_id_key";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$declare
|
||||
k text;
|
||||
old_val jsonb;
|
||||
new_val jsonb;
|
||||
|
||||
v_response_id text;
|
||||
begin
|
||||
v_response_id := nullif(new.meta_origen->>'response_id','');
|
||||
|
||||
-- INSERT -> CREACION
|
||||
if tg_op = 'INSERT' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.creado_por,
|
||||
'CREACION'::public.tipo_cambio,
|
||||
null,
|
||||
null,
|
||||
to_jsonb(new),
|
||||
null
|
||||
);
|
||||
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- DELETE (opcional): si no lo quieres, bórralo
|
||||
if tg_op = 'DELETE' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
old.id,
|
||||
old.actualizado_por,
|
||||
'OTRO'::public.tipo_cambio,
|
||||
'DELETE',
|
||||
to_jsonb(old),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
return old;
|
||||
end if;
|
||||
|
||||
-- UPDATE ----------------------------------------------------------
|
||||
-- 1) Transición de estado
|
||||
if (new.estado_actual_id is distinct from old.estado_actual_id) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'TRANSICION_ESTADO'::public.tipo_cambio,
|
||||
'estado_actual_id',
|
||||
to_jsonb(old.estado_actual_id),
|
||||
to_jsonb(new.estado_actual_id),
|
||||
null
|
||||
);
|
||||
end if;
|
||||
|
||||
-- 2) Cambios en JSONB "datos" (diff top-level por llave)
|
||||
if (new.datos is distinct from old.datos) then
|
||||
for k in
|
||||
select distinct key
|
||||
from (
|
||||
select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key
|
||||
union all
|
||||
select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key
|
||||
) t
|
||||
loop
|
||||
old_val := coalesce(old.datos, '{}'::jsonb) -> k;
|
||||
new_val := coalesce(new.datos, '{}'::jsonb) -> k;
|
||||
|
||||
if (old_val is distinct from new_val) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'ACTUALIZACION_CAMPO'::public.tipo_cambio,
|
||||
k,
|
||||
old_val,
|
||||
new_val,
|
||||
v_response_id
|
||||
);
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
end if;
|
||||
|
||||
-- 3) Cambios en columnas "normales" (uno por columna)
|
||||
if (new.nombre is distinct from old.nombre) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null);
|
||||
end if;
|
||||
|
||||
if (new.nivel is distinct from old.nivel) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_ciclo is distinct from old.tipo_ciclo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null);
|
||||
end if;
|
||||
|
||||
if (new.numero_ciclos is distinct from old.numero_ciclos) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null);
|
||||
end if;
|
||||
|
||||
if (new.activo is distinct from old.activo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null);
|
||||
end if;
|
||||
|
||||
if (new.carrera_id is distinct from old.carrera_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null);
|
||||
end if;
|
||||
|
||||
if (new.estructura_id is distinct from old.estructura_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_origen is distinct from old.tipo_origen) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null);
|
||||
end if;
|
||||
|
||||
|
||||
if (new.conversation_id is distinct from old.conversation_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null);
|
||||
end if;
|
||||
|
||||
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
||||
if v_response_id is not null then
|
||||
new.meta_origen := new.meta_origen - 'response_id';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;$function$
|
||||
;
|
||||
|
||||
CREATE TRIGGER "agregar-conversation_id-asignaturas" AFTER INSERT ON public.asignaturas FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000');
|
||||
|
||||
CREATE TRIGGER "agregar-conversation_id-planes_estudio" AFTER INSERT ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000');
|
||||
|
||||
CREATE TRIGGER trg_planes_estudio_log_cambios AFTER INSERT OR DELETE OR UPDATE ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION public.fn_log_cambios_planes_estudio();
|
||||
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
create type "public"."estado_conversacion" as enum ('ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR');
|
||||
|
||||
alter table "public"."planes_estudio" drop constraint "planes_estudio_conversation_id_key";
|
||||
|
||||
drop view if exists "public"."plantilla_plan";
|
||||
|
||||
drop index if exists "public"."planes_estudio_conversation_id_key";
|
||||
|
||||
|
||||
create table "public"."conversaciones_asignatura" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"asignatura_id" uuid not null,
|
||||
"openai_conversation_id" text not null,
|
||||
"estado" public.estado_conversacion not null default 'ACTIVA'::public.estado_conversacion,
|
||||
"conversacion_json" jsonb not null default '{}'::jsonb,
|
||||
"creado_por" uuid,
|
||||
"creado_en" timestamp with time zone not null default now(),
|
||||
"archivado_por" uuid,
|
||||
"archivado_en" timestamp with time zone,
|
||||
"intento_archivado" integer not null default 0
|
||||
);
|
||||
|
||||
|
||||
|
||||
create table "public"."conversaciones_plan" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"plan_estudio_id" uuid not null,
|
||||
"openai_conversation_id" text not null,
|
||||
"estado" public.estado_conversacion not null default 'ACTIVA'::public.estado_conversacion,
|
||||
"conversacion_json" jsonb not null default '{}'::jsonb,
|
||||
"creado_por" uuid,
|
||||
"creado_en" timestamp with time zone not null default now(),
|
||||
"archivado_por" uuid,
|
||||
"archivado_en" timestamp with time zone,
|
||||
"intento_archivado" integer not null default 0
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."planes_estudio" drop column "conversation_id";
|
||||
|
||||
CREATE UNIQUE INDEX conversaciones_asignatura_openai_id_unico ON public.conversaciones_asignatura USING btree (openai_conversation_id);
|
||||
|
||||
CREATE UNIQUE INDEX conversaciones_asignatura_pkey ON public.conversaciones_asignatura USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX conversaciones_plan_openai_id_unico ON public.conversaciones_plan USING btree (openai_conversation_id);
|
||||
|
||||
CREATE UNIQUE INDEX conversaciones_plan_pkey ON public.conversaciones_plan USING btree (id);
|
||||
|
||||
CREATE INDEX idx_conv_asig_asignatura ON public.conversaciones_asignatura USING btree (asignatura_id);
|
||||
|
||||
CREATE INDEX idx_conv_asig_estado ON public.conversaciones_asignatura USING btree (estado);
|
||||
|
||||
CREATE INDEX idx_conv_plan_estado ON public.conversaciones_plan USING btree (estado);
|
||||
|
||||
CREATE INDEX idx_conv_plan_plan_estudio ON public.conversaciones_plan USING btree (plan_estudio_id);
|
||||
|
||||
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_pkey" PRIMARY KEY using index "conversaciones_asignatura_pkey";
|
||||
|
||||
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_pkey" PRIMARY KEY using index "conversaciones_plan_pkey";
|
||||
|
||||
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_archivado_por_fkey" FOREIGN KEY (archivado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||
|
||||
alter table "public"."conversaciones_asignatura" validate constraint "conversaciones_asignatura_archivado_por_fkey";
|
||||
|
||||
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_asignatura_id_fkey" FOREIGN KEY (asignatura_id) REFERENCES public.asignaturas(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."conversaciones_asignatura" validate constraint "conversaciones_asignatura_asignatura_id_fkey";
|
||||
|
||||
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_creado_por_fkey" FOREIGN KEY (creado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||
|
||||
alter table "public"."conversaciones_asignatura" validate constraint "conversaciones_asignatura_creado_por_fkey";
|
||||
|
||||
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_openai_id_unico" UNIQUE using index "conversaciones_asignatura_openai_id_unico";
|
||||
|
||||
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_archivado_por_fkey" FOREIGN KEY (archivado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||
|
||||
alter table "public"."conversaciones_plan" validate constraint "conversaciones_plan_archivado_por_fkey";
|
||||
|
||||
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_creado_por_fkey" FOREIGN KEY (creado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||
|
||||
alter table "public"."conversaciones_plan" validate constraint "conversaciones_plan_creado_por_fkey";
|
||||
|
||||
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_openai_id_unico" UNIQUE using index "conversaciones_plan_openai_id_unico";
|
||||
|
||||
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_plan_estudio_id_fkey" FOREIGN KEY (plan_estudio_id) REFERENCES public.planes_estudio(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."conversaciones_plan" validate constraint "conversaciones_plan_plan_estudio_id_fkey";
|
||||
|
||||
create or replace view "public"."plantilla_plan" as SELECT plan.id AS plan_estudio_id,
|
||||
struct.id AS estructura_id,
|
||||
struct.template_id
|
||||
FROM (public.planes_estudio plan
|
||||
JOIN public.estructuras_plan struct ON ((plan.estructura_id = struct.id)));
|
||||
|
||||
|
||||
grant delete on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant insert on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant references on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant select on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant trigger on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant truncate on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant update on table "public"."conversaciones_asignatura" to "anon";
|
||||
|
||||
grant delete on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant insert on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant references on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant select on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant update on table "public"."conversaciones_asignatura" to "authenticated";
|
||||
|
||||
grant delete on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant insert on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant references on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant select on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant trigger on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant truncate on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant update on table "public"."conversaciones_asignatura" to "postgres";
|
||||
|
||||
grant delete on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant insert on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant references on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant select on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant trigger on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant truncate on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant update on table "public"."conversaciones_asignatura" to "service_role";
|
||||
|
||||
grant delete on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant insert on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant references on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant select on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant trigger on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant truncate on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant update on table "public"."conversaciones_plan" to "anon";
|
||||
|
||||
grant delete on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant insert on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant references on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant select on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant update on table "public"."conversaciones_plan" to "authenticated";
|
||||
|
||||
grant delete on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant insert on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant references on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant select on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant trigger on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant truncate on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant update on table "public"."conversaciones_plan" to "postgres";
|
||||
|
||||
grant delete on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
grant insert on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
grant references on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
grant select on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
grant trigger on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
grant truncate on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
grant update on table "public"."conversaciones_plan" to "service_role";
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
drop trigger if exists "agregar-conversation_id-asignaturas" on "public"."asignaturas";
|
||||
|
||||
drop trigger if exists "agregar-conversation_id-planes_estudio" on "public"."planes_estudio";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$declare
|
||||
k text;
|
||||
old_val jsonb;
|
||||
new_val jsonb;
|
||||
|
||||
v_response_id text;
|
||||
begin
|
||||
v_response_id := nullif(new.meta_origen->>'response_id','');
|
||||
|
||||
-- INSERT -> CREACION
|
||||
if tg_op = 'INSERT' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.creado_por,
|
||||
'CREACION'::public.tipo_cambio,
|
||||
null,
|
||||
null,
|
||||
to_jsonb(new),
|
||||
null
|
||||
);
|
||||
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- DELETE (opcional): si no lo quieres, bórralo
|
||||
if tg_op = 'DELETE' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
old.id,
|
||||
old.actualizado_por,
|
||||
'OTRO'::public.tipo_cambio,
|
||||
'DELETE',
|
||||
to_jsonb(old),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
return old;
|
||||
end if;
|
||||
|
||||
-- UPDATE ----------------------------------------------------------
|
||||
-- 1) Transición de estado
|
||||
if (new.estado_actual_id is distinct from old.estado_actual_id) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'TRANSICION_ESTADO'::public.tipo_cambio,
|
||||
'estado_actual_id',
|
||||
to_jsonb(old.estado_actual_id),
|
||||
to_jsonb(new.estado_actual_id),
|
||||
null
|
||||
);
|
||||
end if;
|
||||
|
||||
-- 2) Cambios en JSONB "datos" (diff top-level por llave)
|
||||
if (new.datos is distinct from old.datos) then
|
||||
for k in
|
||||
select distinct key
|
||||
from (
|
||||
select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key
|
||||
union all
|
||||
select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key
|
||||
) t
|
||||
loop
|
||||
old_val := coalesce(old.datos, '{}'::jsonb) -> k;
|
||||
new_val := coalesce(new.datos, '{}'::jsonb) -> k;
|
||||
|
||||
if (old_val is distinct from new_val) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'ACTUALIZACION_CAMPO'::public.tipo_cambio,
|
||||
k,
|
||||
old_val,
|
||||
new_val,
|
||||
v_response_id
|
||||
);
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
end if;
|
||||
|
||||
-- 3) Cambios en columnas "normales" (uno por columna)
|
||||
if (new.nombre is distinct from old.nombre) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null);
|
||||
end if;
|
||||
|
||||
if (new.nivel is distinct from old.nivel) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_ciclo is distinct from old.tipo_ciclo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null);
|
||||
end if;
|
||||
|
||||
if (new.numero_ciclos is distinct from old.numero_ciclos) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null);
|
||||
end if;
|
||||
|
||||
if (new.activo is distinct from old.activo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null);
|
||||
end if;
|
||||
|
||||
if (new.carrera_id is distinct from old.carrera_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null);
|
||||
end if;
|
||||
|
||||
if (new.estructura_id is distinct from old.estructura_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_origen is distinct from old.tipo_origen) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null);
|
||||
end if;
|
||||
|
||||
|
||||
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
||||
if v_response_id is not null then
|
||||
new.meta_origen := new.meta_origen - 'response_id';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;$function$
|
||||
;
|
||||
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
create type "public"."estado_asignatura" as enum ('borrador', 'revisada', 'aprobada', 'generando');
|
||||
|
||||
alter table "public"."asignaturas" add column "estado" public.estado_asignatura not null default 'generando'::public.estado_asignatura;
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$declare
|
||||
k text;
|
||||
old_val jsonb;
|
||||
new_val jsonb;
|
||||
|
||||
v_response_id text;
|
||||
begin
|
||||
v_response_id := nullif(new.meta_origen->>'response_id','');
|
||||
|
||||
-- INSERT -> CREACION
|
||||
if tg_op = 'INSERT' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.creado_por,
|
||||
'CREACION'::public.tipo_cambio,
|
||||
null,
|
||||
null,
|
||||
to_jsonb(new),
|
||||
null
|
||||
);
|
||||
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- DELETE (opcional): si no lo quieres, bórralo
|
||||
if tg_op = 'DELETE' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
old.id,
|
||||
old.actualizado_por,
|
||||
'OTRO'::public.tipo_cambio,
|
||||
'DELETE',
|
||||
to_jsonb(old),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
return old;
|
||||
end if;
|
||||
|
||||
-- UPDATE ----------------------------------------------------------
|
||||
-- 1) Transición de estado
|
||||
if (new.estado_actual_id is distinct from old.estado_actual_id) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'TRANSICION_ESTADO'::public.tipo_cambio,
|
||||
'estado_actual_id',
|
||||
to_jsonb(old.estado_actual_id),
|
||||
to_jsonb(new.estado_actual_id),
|
||||
null
|
||||
);
|
||||
end if;
|
||||
|
||||
-- 2) Cambios en JSONB "datos" (diff top-level por llave)
|
||||
if (new.datos is distinct from old.datos) then
|
||||
for k in
|
||||
select distinct key
|
||||
from (
|
||||
select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key
|
||||
union all
|
||||
select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key
|
||||
) t
|
||||
loop
|
||||
old_val := coalesce(old.datos, '{}'::jsonb) -> k;
|
||||
new_val := coalesce(new.datos, '{}'::jsonb) -> k;
|
||||
|
||||
if (old_val is distinct from new_val) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'ACTUALIZACION_CAMPO'::public.tipo_cambio,
|
||||
k,
|
||||
old_val,
|
||||
new_val,
|
||||
v_response_id
|
||||
);
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
end if;
|
||||
|
||||
-- 3) Cambios en columnas "normales" (uno por columna)
|
||||
if (new.nombre is distinct from old.nombre) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null);
|
||||
end if;
|
||||
|
||||
if (new.nivel is distinct from old.nivel) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_ciclo is distinct from old.tipo_ciclo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null);
|
||||
end if;
|
||||
|
||||
if (new.numero_ciclos is distinct from old.numero_ciclos) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null);
|
||||
end if;
|
||||
|
||||
if (new.activo is distinct from old.activo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null);
|
||||
end if;
|
||||
|
||||
if (new.carrera_id is distinct from old.carrera_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null);
|
||||
end if;
|
||||
|
||||
if (new.estructura_id is distinct from old.estructura_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_origen is distinct from old.tipo_origen) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null);
|
||||
end if;
|
||||
|
||||
|
||||
if (new.conversation_id is distinct from old.conversation_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null);
|
||||
end if;
|
||||
|
||||
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
||||
if v_response_id is not null then
|
||||
new.meta_origen := new.meta_origen - 'response_id';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;$function$
|
||||
;
|
||||
|
||||
|
||||
+452
-16
@@ -12,6 +12,13 @@ SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgsodium";
|
||||
|
||||
|
||||
@@ -23,6 +30,13 @@ COMMENT ON SCHEMA "public" IS 'standard public schema';
|
||||
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "http" WITH SCHEMA "extensions";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
|
||||
|
||||
|
||||
@@ -72,6 +86,28 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
|
||||
|
||||
|
||||
|
||||
CREATE TYPE "public"."estado_asignatura" AS ENUM (
|
||||
'borrador',
|
||||
'revisada',
|
||||
'aprobada',
|
||||
'generando'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE "public"."estado_asignatura" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE TYPE "public"."estado_conversacion" AS ENUM (
|
||||
'ACTIVA',
|
||||
'ARCHIVANDO',
|
||||
'ARCHIVADA',
|
||||
'ERROR'
|
||||
);
|
||||
|
||||
|
||||
ALTER TYPE "public"."estado_conversacion" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE TYPE "public"."estado_tarea_revision" AS ENUM (
|
||||
'PENDIENTE',
|
||||
'COMPLETADA',
|
||||
@@ -151,7 +187,9 @@ CREATE TYPE "public"."tipo_cambio" AS ENUM (
|
||||
'ACTUALIZACION_CAMPO',
|
||||
'ACTUALIZACION_MAPA',
|
||||
'TRANSICION_ESTADO',
|
||||
'OTRO'
|
||||
'OTRO',
|
||||
'CREACION',
|
||||
'ACTUALIZACION'
|
||||
);
|
||||
|
||||
|
||||
@@ -222,6 +260,168 @@ CREATE TYPE "public"."tipo_origen" AS ENUM (
|
||||
ALTER TYPE "public"."tipo_origen" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."fn_log_cambios_planes_estudio"() RETURNS "trigger"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$declare
|
||||
k text;
|
||||
old_val jsonb;
|
||||
new_val jsonb;
|
||||
|
||||
v_response_id text;
|
||||
begin
|
||||
v_response_id := nullif(new.meta_origen->>'response_id','');
|
||||
|
||||
-- INSERT -> CREACION
|
||||
if tg_op = 'INSERT' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.creado_por,
|
||||
'CREACION'::public.tipo_cambio,
|
||||
null,
|
||||
null,
|
||||
to_jsonb(new),
|
||||
null
|
||||
);
|
||||
|
||||
return new;
|
||||
end if;
|
||||
|
||||
-- DELETE (opcional): si no lo quieres, bórralo
|
||||
if tg_op = 'DELETE' then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id,
|
||||
cambiado_por,
|
||||
tipo,
|
||||
campo,
|
||||
valor_anterior,
|
||||
valor_nuevo,
|
||||
response_id
|
||||
)
|
||||
values (
|
||||
old.id,
|
||||
old.actualizado_por,
|
||||
'OTRO'::public.tipo_cambio,
|
||||
'DELETE',
|
||||
to_jsonb(old),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
return old;
|
||||
end if;
|
||||
|
||||
-- UPDATE ----------------------------------------------------------
|
||||
-- 1) Transición de estado
|
||||
if (new.estado_actual_id is distinct from old.estado_actual_id) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'TRANSICION_ESTADO'::public.tipo_cambio,
|
||||
'estado_actual_id',
|
||||
to_jsonb(old.estado_actual_id),
|
||||
to_jsonb(new.estado_actual_id),
|
||||
null
|
||||
);
|
||||
end if;
|
||||
|
||||
-- 2) Cambios en JSONB "datos" (diff top-level por llave)
|
||||
if (new.datos is distinct from old.datos) then
|
||||
for k in
|
||||
select distinct key
|
||||
from (
|
||||
select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key
|
||||
union all
|
||||
select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key
|
||||
) t
|
||||
loop
|
||||
old_val := coalesce(old.datos, '{}'::jsonb) -> k;
|
||||
new_val := coalesce(new.datos, '{}'::jsonb) -> k;
|
||||
|
||||
if (old_val is distinct from new_val) then
|
||||
insert into public.cambios_plan (
|
||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.actualizado_por,
|
||||
'ACTUALIZACION_CAMPO'::public.tipo_cambio,
|
||||
k,
|
||||
old_val,
|
||||
new_val,
|
||||
v_response_id
|
||||
);
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
end if;
|
||||
|
||||
-- 3) Cambios en columnas "normales" (uno por columna)
|
||||
if (new.nombre is distinct from old.nombre) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null);
|
||||
end if;
|
||||
|
||||
if (new.nivel is distinct from old.nivel) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_ciclo is distinct from old.tipo_ciclo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null);
|
||||
end if;
|
||||
|
||||
if (new.numero_ciclos is distinct from old.numero_ciclos) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null);
|
||||
end if;
|
||||
|
||||
if (new.activo is distinct from old.activo) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null);
|
||||
end if;
|
||||
|
||||
if (new.carrera_id is distinct from old.carrera_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null);
|
||||
end if;
|
||||
|
||||
if (new.estructura_id is distinct from old.estructura_id) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null);
|
||||
end if;
|
||||
|
||||
if (new.tipo_origen is distinct from old.tipo_origen) then
|
||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null);
|
||||
end if;
|
||||
|
||||
|
||||
|
||||
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
||||
if v_response_id is not null then
|
||||
new.meta_origen := new.meta_origen - 'response_id';
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;$$;
|
||||
|
||||
|
||||
ALTER FUNCTION "public"."fn_log_cambios_planes_estudio"() OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."set_actualizado_en"() RETURNS "trigger"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
@@ -305,12 +505,10 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"plan_estudio_id" "uuid" NOT NULL,
|
||||
"estructura_id" "uuid",
|
||||
"facultad_propietaria_id" "uuid",
|
||||
"codigo" "text",
|
||||
"nombre" "text" NOT NULL,
|
||||
"tipo" "public"."tipo_asignatura" DEFAULT 'OBLIGATORIA'::"public"."tipo_asignatura" NOT NULL,
|
||||
"creditos" numeric NOT NULL,
|
||||
"horas_semana" integer,
|
||||
"numero_ciclo" integer,
|
||||
"linea_plan_id" "uuid",
|
||||
"orden_celda" integer,
|
||||
@@ -322,9 +520,14 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
||||
"actualizado_por" "uuid",
|
||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"actualizado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"horas_academicas" integer,
|
||||
"horas_independientes" integer,
|
||||
"asignatura_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED,
|
||||
"estado" "public"."estado_asignatura" DEFAULT 'borrador'::"public"."estado_asignatura" NOT NULL,
|
||||
CONSTRAINT "asignaturas_ciclo_chk" CHECK ((("numero_ciclo" IS NULL) OR ("numero_ciclo" > 0))),
|
||||
CONSTRAINT "asignaturas_creditos_check" CHECK (("creditos" >= (0)::numeric)),
|
||||
CONSTRAINT "asignaturas_horas_semana_check" CHECK ((("horas_semana" IS NULL) OR ("horas_semana" >= 0))),
|
||||
CONSTRAINT "asignaturas_horas_academicas_check" CHECK ((("horas_academicas" IS NULL) OR ("horas_academicas" >= 0))),
|
||||
CONSTRAINT "asignaturas_horas_independientes_check" CHECK ((("horas_independientes" IS NULL) OR ("horas_independientes" >= 0))),
|
||||
CONSTRAINT "asignaturas_orden_celda_chk" CHECK ((("orden_celda" IS NULL) OR ("orden_celda" >= 0)))
|
||||
);
|
||||
|
||||
@@ -374,7 +577,7 @@ CREATE TABLE IF NOT EXISTS "public"."cambios_plan" (
|
||||
"campo" "text",
|
||||
"valor_anterior" "jsonb",
|
||||
"valor_nuevo" "jsonb",
|
||||
"interaccion_ia_id" "uuid"
|
||||
"response_id" "text"
|
||||
);
|
||||
|
||||
|
||||
@@ -396,6 +599,40 @@ CREATE TABLE IF NOT EXISTS "public"."carreras" (
|
||||
ALTER TABLE "public"."carreras" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."conversaciones_asignatura" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"asignatura_id" "uuid" NOT NULL,
|
||||
"openai_conversation_id" "text" NOT NULL,
|
||||
"estado" "public"."estado_conversacion" DEFAULT 'ACTIVA'::"public"."estado_conversacion" NOT NULL,
|
||||
"conversacion_json" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
|
||||
"creado_por" "uuid",
|
||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"archivado_por" "uuid",
|
||||
"archivado_en" timestamp with time zone,
|
||||
"intento_archivado" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE "public"."conversaciones_asignatura" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."conversaciones_plan" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"plan_estudio_id" "uuid" NOT NULL,
|
||||
"openai_conversation_id" "text" NOT NULL,
|
||||
"estado" "public"."estado_conversacion" DEFAULT 'ACTIVA'::"public"."estado_conversacion" NOT NULL,
|
||||
"conversacion_json" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
|
||||
"creado_por" "uuid",
|
||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"archivado_por" "uuid",
|
||||
"archivado_en" timestamp with time zone,
|
||||
"intento_archivado" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE "public"."conversaciones_plan" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."estados_plan" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"clave" "text" NOT NULL,
|
||||
@@ -517,6 +754,7 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" (
|
||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"actualizado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"nombre_search" "text" GENERATED ALWAYS AS ("lower"("public"."unaccent_immutable"("nombre"))) STORED,
|
||||
"plan_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED,
|
||||
CONSTRAINT "planes_estudio_numero_ciclos_check" CHECK (("numero_ciclos" > 0))
|
||||
);
|
||||
|
||||
@@ -524,6 +762,17 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" (
|
||||
ALTER TABLE "public"."planes_estudio" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE OR REPLACE VIEW "public"."plantilla_plan" AS
|
||||
SELECT "plan"."id" AS "plan_estudio_id",
|
||||
"struct"."id" AS "estructura_id",
|
||||
"struct"."template_id"
|
||||
FROM ("public"."planes_estudio" "plan"
|
||||
JOIN "public"."estructuras_plan" "struct" ON (("plan"."estructura_id" = "struct"."id")));
|
||||
|
||||
|
||||
ALTER TABLE "public"."plantilla_plan" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."responsables_asignatura" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"asignatura_id" "uuid" NOT NULL,
|
||||
@@ -645,6 +894,26 @@ ALTER TABLE ONLY "public"."carreras"
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_asignatura"
|
||||
ADD CONSTRAINT "conversaciones_asignatura_openai_id_unico" UNIQUE ("openai_conversation_id");
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_asignatura"
|
||||
ADD CONSTRAINT "conversaciones_asignatura_pkey" PRIMARY KEY ("id");
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_plan"
|
||||
ADD CONSTRAINT "conversaciones_plan_openai_id_unico" UNIQUE ("openai_conversation_id");
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_plan"
|
||||
ADD CONSTRAINT "conversaciones_plan_pkey" PRIMARY KEY ("id");
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."estados_plan"
|
||||
ADD CONSTRAINT "estados_plan_clave_key" UNIQUE ("clave");
|
||||
|
||||
@@ -771,6 +1040,22 @@ CREATE INDEX "bibliografia_asignatura_idx" ON "public"."bibliografia_asignatura"
|
||||
|
||||
|
||||
|
||||
CREATE INDEX "idx_conv_asig_asignatura" ON "public"."conversaciones_asignatura" USING "btree" ("asignatura_id");
|
||||
|
||||
|
||||
|
||||
CREATE INDEX "idx_conv_asig_estado" ON "public"."conversaciones_asignatura" USING "btree" ("estado");
|
||||
|
||||
|
||||
|
||||
CREATE INDEX "idx_conv_plan_estado" ON "public"."conversaciones_plan" USING "btree" ("estado");
|
||||
|
||||
|
||||
|
||||
CREATE INDEX "idx_conv_plan_plan_estudio" ON "public"."conversaciones_plan" USING "btree" ("plan_estudio_id");
|
||||
|
||||
|
||||
|
||||
CREATE INDEX "idx_planes_nombre_search" ON "public"."planes_estudio" USING "btree" ("nombre_search");
|
||||
|
||||
|
||||
@@ -807,6 +1092,10 @@ CREATE OR REPLACE TRIGGER "trg_planes_estudio_actualizado_en" BEFORE UPDATE ON "
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE TRIGGER "trg_planes_estudio_log_cambios" AFTER INSERT OR DELETE OR UPDATE ON "public"."planes_estudio" FOR EACH ROW EXECUTE FUNCTION "public"."fn_log_cambios_planes_estudio"();
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE TRIGGER "trg_usuarios_app_actualizado_en" BEFORE UPDATE ON "public"."usuarios_app" FOR EACH ROW EXECUTE FUNCTION "public"."set_actualizado_en"();
|
||||
|
||||
|
||||
@@ -835,11 +1124,6 @@ ALTER TABLE ONLY "public"."asignaturas"
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."asignaturas"
|
||||
ADD CONSTRAINT "asignaturas_facultad_propietaria_id_fkey" FOREIGN KEY ("facultad_propietaria_id") REFERENCES "public"."facultades"("id") ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."asignaturas"
|
||||
ADD CONSTRAINT "asignaturas_linea_plan_fk_compuesta" FOREIGN KEY ("linea_plan_id", "plan_estudio_id") REFERENCES "public"."lineas_plan"("id", "plan_estudio_id") ON DELETE SET NULL;
|
||||
|
||||
@@ -875,16 +1159,41 @@ ALTER TABLE ONLY "public"."cambios_plan"
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."cambios_plan"
|
||||
ADD CONSTRAINT "cambios_plan_plan_estudio_id_fkey" FOREIGN KEY ("plan_estudio_id") REFERENCES "public"."planes_estudio"("id") ON DELETE CASCADE;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."carreras"
|
||||
ADD CONSTRAINT "carreras_facultad_id_fkey" FOREIGN KEY ("facultad_id") REFERENCES "public"."facultades"("id") ON DELETE RESTRICT;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_asignatura"
|
||||
ADD CONSTRAINT "conversaciones_asignatura_archivado_por_fkey" FOREIGN KEY ("archivado_por") REFERENCES "public"."usuarios_app"("id") ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_asignatura"
|
||||
ADD CONSTRAINT "conversaciones_asignatura_asignatura_id_fkey" FOREIGN KEY ("asignatura_id") REFERENCES "public"."asignaturas"("id") ON DELETE CASCADE;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_asignatura"
|
||||
ADD CONSTRAINT "conversaciones_asignatura_creado_por_fkey" FOREIGN KEY ("creado_por") REFERENCES "public"."usuarios_app"("id") ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_plan"
|
||||
ADD CONSTRAINT "conversaciones_plan_archivado_por_fkey" FOREIGN KEY ("archivado_por") REFERENCES "public"."usuarios_app"("id") ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_plan"
|
||||
ADD CONSTRAINT "conversaciones_plan_creado_por_fkey" FOREIGN KEY ("creado_por") REFERENCES "public"."usuarios_app"("id") ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."conversaciones_plan"
|
||||
ADD CONSTRAINT "conversaciones_plan_plan_estudio_id_fkey" FOREIGN KEY ("plan_estudio_id") REFERENCES "public"."planes_estudio"("id") ON DELETE CASCADE;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."interacciones_ia"
|
||||
ADD CONSTRAINT "interacciones_ia_asignatura_id_fkey" FOREIGN KEY ("asignatura_id") REFERENCES "public"."asignaturas"("id") ON DELETE CASCADE;
|
||||
|
||||
@@ -1013,7 +1322,6 @@ ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres";
|
||||
|
||||
|
||||
|
||||
|
||||
GRANT USAGE ON SCHEMA "public" TO "postgres";
|
||||
GRANT USAGE ON SCHEMA "public" TO "anon";
|
||||
GRANT USAGE ON SCHEMA "public" TO "authenticated";
|
||||
@@ -1198,10 +1506,115 @@ GRANT USAGE ON SCHEMA "public" TO "service_role";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "postgres";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "postgres";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_immutable"("text") TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_immutable"("text") TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_immutable"("text") TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "postgres";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "postgres";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "service_role";
|
||||
@@ -1268,6 +1681,18 @@ GRANT ALL ON TABLE "public"."carreras" TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON TABLE "public"."conversaciones_asignatura" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."conversaciones_asignatura" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."conversaciones_asignatura" TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON TABLE "public"."conversaciones_plan" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."conversaciones_plan" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."conversaciones_plan" TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON TABLE "public"."estados_plan" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."estados_plan" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."estados_plan" TO "service_role";
|
||||
@@ -1316,6 +1741,12 @@ GRANT ALL ON TABLE "public"."planes_estudio" TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON TABLE "public"."plantilla_plan" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."plantilla_plan" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."plantilla_plan" TO "service_role";
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON TABLE "public"."responsables_asignatura" TO "anon";
|
||||
GRANT ALL ON TABLE "public"."responsables_asignatura" TO "authenticated";
|
||||
GRANT ALL ON TABLE "public"."responsables_asignatura" TO "service_role";
|
||||
@@ -1417,4 +1848,9 @@ ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TAB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Dumped schema changes for auth and storage
|
||||
--
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
alter table "public"."conversaciones_plan" add column "nombre" text default ('Chat '::text || CURRENT_DATE);
|
||||
|
||||
alter table "public"."conversaciones_plan" alter column "conversacion_json" set default '[]'::jsonb;
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE OR REPLACE FUNCTION public.append_conversacion_asignatura(p_id uuid, p_append jsonb)
|
||||
RETURNS void
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
update conversaciones_asignatura
|
||||
set conversacion_json = coalesce(conversacion_json, '[]'::jsonb) || p_append
|
||||
where id = p_id;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.append_conversacion_plan(p_id uuid, p_append jsonb)
|
||||
RETURNS void
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
update conversaciones_plan
|
||||
set conversacion_json = coalesce(conversacion_json, '[]'::jsonb) || p_append
|
||||
where id = p_id;
|
||||
$function$
|
||||
;
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
alter table "public"."asignaturas" alter column "estado" drop default;
|
||||
|
||||
alter type "public"."estado_asignatura" rename to "estado_asignatura__old_version_to_be_dropped";
|
||||
|
||||
create type "public"."estado_asignatura" as enum ('borrador', 'revisada', 'aprobada', 'generando', 'fallida');
|
||||
|
||||
alter table "public"."asignaturas" alter column estado type "public"."estado_asignatura" using estado::text::"public"."estado_asignatura";
|
||||
|
||||
alter table "public"."asignaturas" alter column "estado" set default 'borrador'::public.estado_asignatura;
|
||||
|
||||
drop type "public"."estado_asignatura__old_version_to_be_dropped";
|
||||
@@ -0,0 +1,192 @@
|
||||
create type "public"."estado_mensaje_ia" as enum ('PROCESANDO', 'COMPLETADO', 'ERROR');
|
||||
|
||||
alter table "public"."asignaturas" alter column "estado" drop default;
|
||||
|
||||
alter type "public"."estado_asignatura" rename to "estado_asignatura__old_version_to_be_dropped";
|
||||
|
||||
create type "public"."estado_asignatura" as enum ('borrador', 'revisada', 'aprobada', 'generando');
|
||||
|
||||
|
||||
create table "public"."asignatura_mensajes_ia" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"enviado_por" uuid not null default auth.uid(),
|
||||
"mensaje" text not null,
|
||||
"campos" text[] not null default '{}'::text[],
|
||||
"respuesta" text,
|
||||
"is_refusal" boolean not null default false,
|
||||
"propuesta" jsonb,
|
||||
"estado" public.estado_mensaje_ia not null default 'PROCESANDO'::public.estado_mensaje_ia,
|
||||
"fecha_creacion" timestamp without time zone not null default now(),
|
||||
"fecha_actualizacion" timestamp without time zone not null default now(),
|
||||
"conversacion_asignatura_id" uuid not null
|
||||
);
|
||||
|
||||
|
||||
|
||||
create table "public"."plan_mensajes_ia" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"enviado_por" uuid not null default auth.uid(),
|
||||
"mensaje" text not null,
|
||||
"campos" text[] not null default '{}'::text[],
|
||||
"respuesta" text,
|
||||
"is_refusal" boolean not null default false,
|
||||
"propuesta" jsonb,
|
||||
"estado" public.estado_mensaje_ia not null default 'PROCESANDO'::public.estado_mensaje_ia,
|
||||
"fecha_creacion" timestamp without time zone not null default now(),
|
||||
"fecha_actualizacion" timestamp without time zone not null default now(),
|
||||
"conversacion_plan_id" uuid not null
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."asignaturas" alter column estado type "public"."estado_asignatura" using estado::text::"public"."estado_asignatura";
|
||||
|
||||
alter table "public"."asignaturas" alter column "estado" set default 'borrador'::public.estado_asignatura;
|
||||
|
||||
drop type "public"."estado_asignatura__old_version_to_be_dropped";
|
||||
|
||||
alter table "public"."estructuras_asignatura" drop column "version";
|
||||
|
||||
alter table "public"."estructuras_asignatura" add column "template_id" text;
|
||||
|
||||
alter table "public"."estructuras_asignatura" add column "tipo" public.tipo_estructura_plan;
|
||||
|
||||
CREATE UNIQUE INDEX asignatura_mensajes_ia_pkey ON public.asignatura_mensajes_ia USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX plan_mensajes_ia_pkey ON public.plan_mensajes_ia USING btree (id);
|
||||
|
||||
alter table "public"."asignatura_mensajes_ia" add constraint "asignatura_mensajes_ia_pkey" PRIMARY KEY using index "asignatura_mensajes_ia_pkey";
|
||||
|
||||
alter table "public"."plan_mensajes_ia" add constraint "plan_mensajes_ia_pkey" PRIMARY KEY using index "plan_mensajes_ia_pkey";
|
||||
|
||||
alter table "public"."asignatura_mensajes_ia" add constraint "asignatura_mensajes_ia_conversacion_asignatura_id_fkey" FOREIGN KEY (conversacion_asignatura_id) REFERENCES public.conversaciones_asignatura(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."asignatura_mensajes_ia" validate constraint "asignatura_mensajes_ia_conversacion_asignatura_id_fkey";
|
||||
|
||||
alter table "public"."plan_mensajes_ia" add constraint "plan_mensajes_ia_conversacion_plan_id_fkey" FOREIGN KEY (conversacion_plan_id) REFERENCES public.conversaciones_plan(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."plan_mensajes_ia" validate constraint "plan_mensajes_ia_conversacion_plan_id_fkey";
|
||||
|
||||
create or replace view "public"."plantilla_asignatura" as SELECT asignaturas.id AS asignatura_id,
|
||||
struct.id AS estructura_id,
|
||||
struct.template_id
|
||||
FROM (public.asignaturas
|
||||
JOIN public.estructuras_asignatura struct ON ((asignaturas.estructura_id = struct.id)));
|
||||
|
||||
|
||||
grant delete on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant insert on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant references on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant select on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant trigger on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant truncate on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant update on table "public"."asignatura_mensajes_ia" to "anon";
|
||||
|
||||
grant delete on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant insert on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant references on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant select on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant update on table "public"."asignatura_mensajes_ia" to "authenticated";
|
||||
|
||||
grant delete on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant insert on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant references on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant select on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant trigger on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant truncate on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant update on table "public"."asignatura_mensajes_ia" to "postgres";
|
||||
|
||||
grant delete on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant insert on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant references on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant select on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant trigger on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant truncate on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant update on table "public"."asignatura_mensajes_ia" to "service_role";
|
||||
|
||||
grant delete on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant insert on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant references on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant select on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant trigger on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant truncate on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant update on table "public"."plan_mensajes_ia" to "anon";
|
||||
|
||||
grant delete on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant insert on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant references on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant select on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant update on table "public"."plan_mensajes_ia" to "authenticated";
|
||||
|
||||
grant delete on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant insert on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant references on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant select on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant trigger on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant truncate on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant update on table "public"."plan_mensajes_ia" to "postgres";
|
||||
|
||||
grant delete on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
grant insert on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
grant references on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
grant select on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
grant trigger on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
grant truncate on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
grant update on table "public"."plan_mensajes_ia" to "service_role";
|
||||
|
||||
drop trigger if exists "protect_buckets_delete" on "storage"."buckets";
|
||||
|
||||
drop trigger if exists "protect_objects_delete" on "storage"."objects";
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."asignaturas" add column "criterios_de_evaluacion" jsonb not null default '[]'::jsonb;
|
||||
+1993
File diff suppressed because it is too large
Load Diff
+217
-26
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
alter publication supabase_realtime drop table asignaturas;
|
||||
Reference in New Issue
Block a user