Actualizar database-types.ts #27
@@ -20,9 +20,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify generated types are checked in
|
- name: Verify generated types are checked in
|
||||||
run: |
|
run: |
|
||||||
supabase gen types typescript --local > database-types.ts
|
supabase gen types typescript --local > supabase/functions/_shared/database.types.ts
|
||||||
if ! git diff --ignore-space-at-eol --exit-code --quiet database-types.ts; then
|
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:"
|
echo "Detected uncommitted changes after build. See status below:"
|
||||||
git diff
|
git diff
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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": [
|
"allowScripts": [
|
||||||
"npm:deno@2.6.4",
|
"npm:deno@2.6.4",
|
||||||
"npm:supabase-js@1.0.4",
|
"npm:supabase-js@1.0.4",
|
||||||
"npm:supabase@2.72.6"
|
"npm:supabase@2.75.0"
|
||||||
],
|
],
|
||||||
"imports": {
|
"imports": {
|
||||||
"@openai/openai": "jsr:@openai/openai@^6.16.0",
|
"@openai/openai": "jsr:@openai/openai@^6.16.0",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"jsr:@david/dax@~0.44.2": "0.44.2",
|
"jsr:@david/dax@~0.44.2": "0.44.2",
|
||||||
"jsr:@david/path@0.2": "0.2.0",
|
"jsr:@david/path@0.2": "0.2.0",
|
||||||
"jsr:@david/which@~0.4.1": "0.4.1",
|
"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",
|
||||||
"jsr:@openai/openai@^6.16.0": "6.16.0",
|
"jsr:@openai/openai@^6.16.0": "6.16.0",
|
||||||
"jsr:@std/bytes@^1.0.6": "1.0.6",
|
"jsr:@std/bytes@^1.0.6": "1.0.6",
|
||||||
@@ -20,12 +21,15 @@
|
|||||||
"jsr:@zod/zod@^4.3.5": "4.3.5",
|
"jsr:@zod/zod@^4.3.5": "4.3.5",
|
||||||
"npm:@supabase/supabase-js@2": "2.90.1",
|
"npm:@supabase/supabase-js@2": "2.90.1",
|
||||||
"npm:@supabase/supabase-js@^2.90.1": "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:@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: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@6.16.0": "6.16.0_zod@3.25.76",
|
||||||
"npm:openai@^4.52.5": "4.104.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: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"
|
"npm:zod@3": "3.25.76"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
@@ -54,6 +58,9 @@
|
|||||||
"@david/which@0.4.1": {
|
"@david/which@0.4.1": {
|
||||||
"integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e"
|
"integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e"
|
||||||
},
|
},
|
||||||
|
"@helpr/express@0.1.1": {
|
||||||
|
"integrity": "15391b350b92ab5102919c555179c954ff3d5974d7f170b99880836e249756d2"
|
||||||
|
},
|
||||||
"@openai/openai@6.16.0": {
|
"@openai/openai@6.16.0": {
|
||||||
"integrity": "ccee548f61c382d715091fff0c2c3390a4487823644430b8235df543d6d6a78b",
|
"integrity": "ccee548f61c382d715091fff0c2c3390a4487823644430b8235df543d6d6a78b",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -182,6 +189,9 @@
|
|||||||
"@supabase/storage-js"
|
"@supabase/storage-js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@toon-format/toon@2.1.0": {
|
||||||
|
"integrity": "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="
|
||||||
|
},
|
||||||
"@types/bun@1.3.5": {
|
"@types/bun@1.3.5": {
|
||||||
"integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==",
|
"integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -254,6 +264,9 @@
|
|||||||
"chownr@3.0.0": {
|
"chownr@3.0.0": {
|
||||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
|
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
|
||||||
},
|
},
|
||||||
|
"citeproc@2.4.63": {
|
||||||
|
"integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q=="
|
||||||
|
},
|
||||||
"cmd-shim@8.0.0": {
|
"cmd-shim@8.0.0": {
|
||||||
"integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="
|
"integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="
|
||||||
},
|
},
|
||||||
@@ -415,6 +428,9 @@
|
|||||||
"imurmurhash@0.1.4": {
|
"imurmurhash@0.1.4": {
|
||||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
|
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
|
||||||
},
|
},
|
||||||
|
"jwt-decode@4.0.0": {
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||||
|
},
|
||||||
"math-intrinsics@1.1.0": {
|
"math-intrinsics@1.1.0": {
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
||||||
},
|
},
|
||||||
@@ -496,8 +512,8 @@
|
|||||||
"signal-exit@4.1.0": {
|
"signal-exit@4.1.0": {
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
||||||
},
|
},
|
||||||
"supabase@2.72.6": {
|
"supabase@2.75.0": {
|
||||||
"integrity": "sha512-axO1MkZgwMIxj78vKiGosnuXwzOaQq0Kz0I8jheNMwJH9dKGf3q/arivYT44z8u3t5FjGUxQWOuQoJetzcmm/A==",
|
"integrity": "sha512-Xaai8Wp7F03OMkOWSyS1OMMdk5DxkauJxcMMesj4mO6jDGEZpWBwgo/gIUb4hsckexmr/RXYdpDpTbUIMXahbw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"bin-links",
|
"bin-links",
|
||||||
"https-proxy-agent",
|
"https-proxy-agent",
|
||||||
@@ -507,8 +523,8 @@
|
|||||||
"scripts": true,
|
"scripts": true,
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"tar@7.5.2": {
|
"tar@7.5.7": {
|
||||||
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@isaacs/fs-minipass",
|
"@isaacs/fs-minipass",
|
||||||
"chownr",
|
"chownr",
|
||||||
@@ -561,6 +577,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remote": {
|
"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/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
||||||
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
||||||
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
||||||
@@ -587,10 +606,13 @@
|
|||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@supabase/supabase-js@^2.90.1",
|
"npm:@supabase/supabase-js@^2.90.1",
|
||||||
|
"npm:@toon-format/toon@^2.1.0",
|
||||||
"npm:@types/bun@^1.3.5",
|
"npm:@types/bun@^1.3.5",
|
||||||
|
"npm:citeproc@^2.4.63",
|
||||||
"npm:deno@^2.6.4",
|
"npm:deno@^2.6.4",
|
||||||
|
"npm:jwt-decode@4",
|
||||||
"npm:openai@^6.16.0",
|
"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": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.90.1",
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
|
"@toon-format/toon": "^2.1.0",
|
||||||
|
"citeproc": "^2.4.63",
|
||||||
"deno": "^2.6.4",
|
"deno": "^2.6.4",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"openai": "^6.16.0",
|
"openai": "^6.16.0",
|
||||||
"supabase": "^2.72.6"
|
"supabase": "2.75.0"
|
||||||
},
|
},
|
||||||
"name": "genesis-2",
|
"name": "genesis-2",
|
||||||
"module": "index.ts",
|
"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]
|
[functions.ai-generate-plan]
|
||||||
enabled = true
|
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.
|
# 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:
|
# For example, if you want to serve static HTML pages in your function:
|
||||||
# static_files = [ "./functions/generate-subject-suggestions/*.html" ]
|
# 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
|
| string
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
@@ -460,6 +460,7 @@ export type Database = {
|
|||||||
estado: Database["public"]["Enums"]["estado_conversacion"]
|
estado: Database["public"]["Enums"]["estado_conversacion"]
|
||||||
id: string
|
id: string
|
||||||
intento_archivado: number
|
intento_archivado: number
|
||||||
|
nombre: string | null
|
||||||
openai_conversation_id: string
|
openai_conversation_id: string
|
||||||
plan_estudio_id: string
|
plan_estudio_id: string
|
||||||
}
|
}
|
||||||
@@ -472,6 +473,7 @@ export type Database = {
|
|||||||
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
||||||
id?: string
|
id?: string
|
||||||
intento_archivado?: number
|
intento_archivado?: number
|
||||||
|
nombre?: string | null
|
||||||
openai_conversation_id: string
|
openai_conversation_id: string
|
||||||
plan_estudio_id: string
|
plan_estudio_id: string
|
||||||
}
|
}
|
||||||
@@ -484,6 +486,7 @@ export type Database = {
|
|||||||
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
||||||
id?: string
|
id?: string
|
||||||
intento_archivado?: number
|
intento_archivado?: number
|
||||||
|
nombre?: string | null
|
||||||
openai_conversation_id?: string
|
openai_conversation_id?: string
|
||||||
plan_estudio_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: {
|
planes_estudio: {
|
||||||
Row: {
|
Row: {
|
||||||
activo: boolean
|
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: {
|
responsables_asignatura: {
|
||||||
Row: {
|
Row: {
|
||||||
asignatura_id: string
|
asignatura_id: string
|
||||||
@@ -961,6 +1015,36 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
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: {
|
tareas_revision: {
|
||||||
Row: {
|
Row: {
|
||||||
asignado_a: string
|
asignado_a: string
|
||||||
@@ -1206,6 +1290,15 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Functions: {
|
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: { Args: { "": string }; Returns: string }
|
||||||
unaccent_immutable: { Args: { "": string }; Returns: string }
|
unaccent_immutable: { Args: { "": string }; Returns: string }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
/// <reference lib="deno.window" />
|
/// <reference lib="deno.window" />
|
||||||
import OpenAI from "npm:openai@6.16.0";
|
import OpenAI from "npm:openai@6.16.0";
|
||||||
import type * as OpenAITypes 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
|
// Use non-streaming params to ensure `responses.create` returns a typed Response
|
||||||
export type StructuredResponseOptions =
|
export type StructuredResponseOptions =
|
||||||
OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming;
|
OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming;
|
||||||
@@ -15,7 +14,6 @@ export type StructuredResponseSuccess<TOutput = unknown> = {
|
|||||||
responseId: string;
|
responseId: string;
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
references: {
|
references: {
|
||||||
uploadedToStorage: string[]; // supabase storage paths
|
|
||||||
openaiFileIds: string[]; // file ids in OpenAI
|
openaiFileIds: string[]; // file ids in OpenAI
|
||||||
};
|
};
|
||||||
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
|
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
|
||||||
@@ -24,7 +22,6 @@ export type StructuredResponseFailure = {
|
|||||||
ok: false;
|
ok: false;
|
||||||
code:
|
code:
|
||||||
| "MissingEnv"
|
| "MissingEnv"
|
||||||
| "StorageUploadFailed"
|
|
||||||
| "OpenAIFileUploadFailed"
|
| "OpenAIFileUploadFailed"
|
||||||
| "OpenAIRequestFailed";
|
| "OpenAIRequestFailed";
|
||||||
message: string;
|
message: string;
|
||||||
@@ -34,40 +31,24 @@ export type StructuredResponseResult<TOutput = unknown> =
|
|||||||
| StructuredResponseSuccess<TOutput>
|
| StructuredResponseSuccess<TOutput>
|
||||||
| StructuredResponseFailure;
|
| StructuredResponseFailure;
|
||||||
export interface OpenAIServiceConfig {
|
export interface OpenAIServiceConfig {
|
||||||
supabaseUrl: string;
|
|
||||||
serviceRoleKey: string;
|
|
||||||
openAIApiKey: string;
|
openAIApiKey: string;
|
||||||
bucket?: string; // default: ai-storage
|
|
||||||
}
|
}
|
||||||
export class OpenAIService {
|
export class OpenAIService {
|
||||||
private readonly supabase: SupabaseClient;
|
|
||||||
private readonly openai: OpenAI;
|
private readonly openai: OpenAI;
|
||||||
private readonly bucket: string;
|
private constructor(openai: OpenAI) {
|
||||||
private constructor(
|
|
||||||
client: SupabaseClient,
|
|
||||||
openai: OpenAI,
|
|
||||||
bucket: string,
|
|
||||||
) {
|
|
||||||
this.supabase = client;
|
|
||||||
this.openai = openai;
|
this.openai = openai;
|
||||||
this.bucket = bucket;
|
|
||||||
}
|
}
|
||||||
static fromEnv(): StructuredResponseFailure | OpenAIService {
|
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 openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? "";
|
||||||
const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage";
|
if (!openAIApiKey) {
|
||||||
if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) {
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
code: "MissingEnv",
|
code: "MissingEnv",
|
||||||
message:
|
message: "Required env vars missing: OPENAI_API_KEY",
|
||||||
"Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const client = createClient(supabaseUrl, serviceRoleKey);
|
|
||||||
const openai = new OpenAI({ apiKey: openAIApiKey });
|
const openai = new OpenAI({ apiKey: openAIApiKey });
|
||||||
return new OpenAIService(client, openai, bucket);
|
return new OpenAIService(openai);
|
||||||
}
|
}
|
||||||
async createConversation(metadata?: Record<string, string>) {
|
async createConversation(metadata?: Record<string, string>) {
|
||||||
const conversation = await this.openai.conversations.create({
|
const conversation = await this.openai.conversations.create({
|
||||||
@@ -80,9 +61,6 @@ export class OpenAIService {
|
|||||||
files?: File[],
|
files?: File[],
|
||||||
): Promise<StructuredResponseResult<TOutput>> {
|
): Promise<StructuredResponseResult<TOutput>> {
|
||||||
try {
|
try {
|
||||||
const uploadedToStorage = await this.uploadFilesToStorage(
|
|
||||||
files ?? [],
|
|
||||||
);
|
|
||||||
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
|
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
|
||||||
|
|
||||||
const newOptions = { ...options };
|
const newOptions = { ...options };
|
||||||
@@ -112,6 +90,11 @@ export class OpenAIService {
|
|||||||
const openaiRaw = (await this.openai.responses.create(
|
const openaiRaw = (await this.openai.responses.create(
|
||||||
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
||||||
)) as OpenAITypes.OpenAI.Responses.Response;
|
)) as OpenAITypes.OpenAI.Responses.Response;
|
||||||
|
|
||||||
|
const isBackground =
|
||||||
|
(newOptions as unknown as { background?: boolean })
|
||||||
|
.background ===
|
||||||
|
true;
|
||||||
const { model, id: responseId } = openaiRaw;
|
const { model, id: responseId } = openaiRaw;
|
||||||
const usage = openaiRaw?.usage ?? null;
|
const usage = openaiRaw?.usage ?? null;
|
||||||
const conversationId = (
|
const conversationId = (
|
||||||
@@ -119,6 +102,22 @@ export class OpenAIService {
|
|||||||
conversation_id?: string | null;
|
conversation_id?: string | null;
|
||||||
}
|
}
|
||||||
).conversation_id ?? 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
|
// Try to read structured JSON output
|
||||||
let output: TOutput | undefined = undefined;
|
let output: TOutput | undefined = undefined;
|
||||||
let outputText: string | undefined = undefined;
|
let outputText: string | undefined = undefined;
|
||||||
@@ -154,38 +153,18 @@ export class OpenAIService {
|
|||||||
usage,
|
usage,
|
||||||
responseId: String(responseId),
|
responseId: String(responseId),
|
||||||
conversationId: conversationId ? String(conversationId) : null,
|
conversationId: conversationId ? String(conversationId) : null,
|
||||||
references: { uploadedToStorage, openaiFileIds },
|
references: { openaiFileIds },
|
||||||
openaiRaw,
|
openaiRaw,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
const message = e?.message ?? "Unknown error";
|
const message = e?.message ?? "Unknown error";
|
||||||
const code = message.includes("Supabase upload failed")
|
const code = message.includes("OpenAI file upload failed")
|
||||||
? "StorageUploadFailed"
|
|
||||||
: message.includes("OpenAI file upload failed")
|
|
||||||
? "OpenAIFileUploadFailed"
|
? "OpenAIFileUploadFailed"
|
||||||
: "OpenAIRequestFailed";
|
: "OpenAIRequestFailed";
|
||||||
return { ok: false, code, message, cause: err };
|
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[]> {
|
private async uploadFilesToOpenAI(files: File[]): Promise<string[]> {
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
for (const file of files) {
|
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 =
|
type TipoCicloType =
|
||||||
Database["public"]["Tables"]["planes_estudio"]["Insert"]["tipo_ciclo"];
|
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> => {
|
Deno.serve(async (req: Request): Promise<Response> => {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const functionName = url.pathname.split("/").pop();
|
const functionName = url.pathname.split("/").pop();
|
||||||
@@ -118,6 +125,10 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|||||||
SERVICE_ROLE_KEY,
|
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 formData = await req.formData();
|
||||||
const validation = parseAndValidate(formData);
|
const validation = parseAndValidate(formData);
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
@@ -184,8 +195,98 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|||||||
? estructuraPlan.definicion as Record<string, unknown>
|
? 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 = {
|
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: [
|
input: [
|
||||||
{ role: "system", content: systemPrompt },
|
{ role: "system", content: systemPrompt },
|
||||||
{ role: "user", content: userPrompt },
|
{ role: "user", content: userPrompt },
|
||||||
@@ -233,126 +334,12 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|||||||
const status = aiResult.code === "MissingEnv" ? 500 : 502;
|
const status = aiResult.code === "MissingEnv" ? 500 : 502;
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
status,
|
status,
|
||||||
"No se pudo generar el plan con IA.",
|
"No se pudo iniciar la generación del plan con IA.",
|
||||||
"OPENAI_REQUEST_FAILED",
|
"OPENAI_REQUEST_FAILED",
|
||||||
aiResult,
|
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
|
// TODO: update a interaccion_ia y e insert a cambios_plan con id de plan generado
|
||||||
|
|
||||||
console.log(
|
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
|
# 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.
|
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";
|
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 = {
|
export type AIGenerateSubjectInput = {
|
||||||
plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
|
datosUpdate: {
|
||||||
datosBasicos: {
|
id?: Tables<"asignaturas">["id"]; // si viene, es update; si no, es insert
|
||||||
nombre: Tables<"asignaturas">["nombre"];
|
plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
|
||||||
|
estructura_id?: Tables<"asignaturas">["estructura_id"] | null;
|
||||||
|
|
||||||
|
nombre?: Tables<"asignaturas">["nombre"];
|
||||||
codigo?: Tables<"asignaturas">["codigo"];
|
codigo?: Tables<"asignaturas">["codigo"];
|
||||||
tipo: Tables<"asignaturas">["tipo"] | null;
|
tipo?: Tables<"asignaturas">["tipo"] | null;
|
||||||
creditos: Tables<"asignaturas">["creditos"] | null;
|
creditos?: Tables<"asignaturas">["creditos"] | null;
|
||||||
horasAcademicas?: Tables<"asignaturas">["horas_academicas"] | null;
|
horas_academicas?: Tables<"asignaturas">["horas_academicas"] | null;
|
||||||
horasIndependientes?: Tables<"asignaturas">["horas_independientes"] | null;
|
horas_independientes?: Tables<"asignaturas">["horas_independientes"] | null;
|
||||||
estructuraId: Tables<"asignaturas">["estructura_id"] | 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?: {
|
iaConfig?: {
|
||||||
descripcionEnfoqueAcademico: string;
|
descripcionEnfoqueAcademico?: string;
|
||||||
instruccionesAdicionalesIA: string;
|
instruccionesAdicionalesIA?: string;
|
||||||
archivosReferencia: Array<string>;
|
archivosAdjuntos?: string[]; // rutas (no File)
|
||||||
repositoriosReferencia?: Array<string>;
|
|
||||||
};
|
};
|
||||||
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 { HttpError, jsonResponse } from "./lib/errors.ts";
|
||||||
import { getOpenAI } from "./lib/openai.ts";
|
import { getOpenAI } from "./lib/openai.ts";
|
||||||
import { getSupabaseServiceClient, requireUser } from "./lib/supabase.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 = {
|
type CreateBody = {
|
||||||
plan_estudio_id: string;
|
plan_estudio_id: string;
|
||||||
@@ -22,6 +23,13 @@ type AddMessageBody = {
|
|||||||
|
|
||||||
const app = new Hono();
|
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
|
// Preflight CORS
|
||||||
app.options(
|
app.options(
|
||||||
"*",
|
"*",
|
||||||
@@ -29,6 +37,13 @@ app.options(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const prefix = "/create-chat-conversation";
|
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 })));
|
app.get(`${prefix}/health`, (c) => withCors(jsonResponse({ ok: true })));
|
||||||
|
|
||||||
@@ -36,7 +51,7 @@ app.get(`${prefix}/health`, (c) => withCors(jsonResponse({ ok: true })));
|
|||||||
* POST /conversations
|
* POST /conversations
|
||||||
* Crea conversación OpenAI + registro en conversaciones_plan
|
* Crea conversación OpenAI + registro en conversaciones_plan
|
||||||
*/
|
*/
|
||||||
app.post(`${prefix}/conversations`, async (c) => {
|
app.post(`${prefix}/plan/conversations`, async (c) => {
|
||||||
try {
|
try {
|
||||||
/* const auth = c.req.header("authorization");
|
/* const auth = c.req.header("authorization");
|
||||||
const user = await requireUser(auth); */
|
const user = await requireUser(auth); */
|
||||||
@@ -108,292 +123,268 @@ app.post(`${prefix}/conversations`, async (c) => {
|
|||||||
return withCors(handleErr(err));
|
return withCors(handleErr(err));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
app.post(`${prefix}/asignatura/conversations`, async (c) => {
|
||||||
/**
|
|
||||||
* GET /conversations/:conversation_plan_id/messages
|
|
||||||
* Lista mensajes (assistant/user) desde OpenAI
|
|
||||||
*/
|
|
||||||
app.get(`${prefix}/conversations/:id/messages`, async (c) => {
|
|
||||||
try {
|
try {
|
||||||
/* const auth = c.req.header("authorization");
|
const body = (await c.req.json().catch(() => ({}))) as Partial<CreateBody>;
|
||||||
await requireUser(auth); */
|
const asignatura_id = body.asignatura_id;
|
||||||
|
assertUuid(asignatura_id ?? "", "asignatura_id");
|
||||||
|
|
||||||
const conversation_plan_id = c.req.param("id");
|
const instanciador = body.instanciador ?? "unknown";
|
||||||
assertUuid(conversation_plan_id, "conversation_plan_id");
|
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 supabase = getSupabaseServiceClient();
|
||||||
const openai = getOpenAI();
|
const openai = getOpenAI();
|
||||||
|
|
||||||
const { data: convRow, error } = await supabase
|
// 1. Verificar que la asignatura existe
|
||||||
.from("conversaciones_plan")
|
const { data: asignatura, error: asigErr } = await supabase
|
||||||
.select("openai_conversation_id, estado")
|
.from("asignaturas")
|
||||||
.eq("id", conversation_plan_id)
|
.select("*")
|
||||||
|
.eq("id", asignatura_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !convRow) {
|
if (asigErr || !asignatura) {
|
||||||
throw new HttpError(
|
throw new HttpError(404, "asignatura_not_found", "Asignatura no encontrada");
|
||||||
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,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = await openai.conversations.items.list(
|
// 2. Crear conversación en OpenAI
|
||||||
convRow.openai_conversation_id,
|
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) =>
|
// 3. Insertar en conversaciones_asignatura (coincidiendo con tu SQL)
|
||||||
it.type === "message" && (it.role === "assistant" || it.role === "user")
|
const { data: row, error: insErr } = await supabase
|
||||||
).map((it: any) => ({
|
.from("conversaciones_asignatura")
|
||||||
role: it.role,
|
.insert({
|
||||||
content: it.content.map((c: any) => c.text).join(""),
|
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) {
|
} catch (err) {
|
||||||
return withCors(handleErr(err));
|
return withCors(handleErr(err));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /conversations/:conversation_plan_id/messages
|
* POST /conversations/:conversation_plan_id/messages
|
||||||
* Agrega mensaje y opcionalmente solicita respuesta estructurada (json_schema)
|
* 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 {
|
try {
|
||||||
/* const auth = c.req.header("authorization");
|
|
||||||
const user = await requireUser(auth); */
|
|
||||||
|
|
||||||
const conversation_plan_id = c.req.param("id");
|
const conversation_plan_id = c.req.param("id");
|
||||||
assertUuid(conversation_plan_id, "conversation_plan_id");
|
assertUuid(conversation_plan_id, "conversation_plan_id");
|
||||||
|
|
||||||
const body = (await c.req.json().catch(() => ({}))) as Partial<
|
const body = (await c.req.json().catch(() => ({}))) as Partial<AddMessageBody>;
|
||||||
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") {
|
if (!body.content || typeof body.content !== "string") {
|
||||||
throw new HttpError(400, "bad_input", "content es requerido");
|
throw new HttpError(400, "bad_input", "content es requerido");
|
||||||
}
|
}
|
||||||
|
|
||||||
const supabase = getSupabaseServiceClient();
|
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
|
const { data: row, error } = await supabase
|
||||||
.from("conversaciones_plan")
|
.from("conversaciones_asignatura")
|
||||||
.select(
|
.select(`id, openai_conversation_id, asignatura_id, asignaturas(*, estructuras_asignatura(definicion))`)
|
||||||
"id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))",
|
.eq("id", conversation_asig_id)
|
||||||
)
|
|
||||||
.eq("id", conversation_plan_id)
|
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error || !row) {
|
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
|
||||||
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",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plan = (row as any).planes_estudio;
|
const asignatura = (row as any).asignaturas;
|
||||||
const definicion = plan?.estructuras_plan?.definicion;
|
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
|
// 2. Insertar el mensaje en estado PROCESANDO (para que el front vea el spinner)
|
||||||
const wantsStructured = !!definicion;
|
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) {
|
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
|
||||||
await openai.responses.create({
|
|
||||||
conversation: row.openai_conversation_id,
|
// 3. Preparar Schema (Usando tu lógica de asignatura)
|
||||||
model: "gpt-5-nano",
|
const schema = isStructured
|
||||||
input: [
|
? pickSchemaAsignaturaFields(definicion, body.campos ?? [])
|
||||||
{
|
: {
|
||||||
role: "system",
|
type: "object",
|
||||||
content: `Este es el plan de estudios actual ${
|
properties: {
|
||||||
JSON.stringify(plan)
|
"ai-message": { type: "string" },
|
||||||
}. Si te hacen una pregunta que no tiene nada que ver con el plan de estudio, responde con un refusal.`,
|
"is_refusal": { type: "boolean" }
|
||||||
},
|
},
|
||||||
{ role: "user", content: body.content },
|
required: ["ai-message", "is_refusal"],
|
||||||
],
|
additionalProperties: false
|
||||||
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.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return withCors(jsonResponse({ ok: true }));
|
// 4. Llamada asincrónica con background: true
|
||||||
}
|
const aiResult = await svc.createStructuredResponse({
|
||||||
|
|
||||||
// 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({
|
|
||||||
conversation: row.openai_conversation_id,
|
conversation: row.openai_conversation_id,
|
||||||
model,
|
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
|
||||||
text: { format: { type: "json_schema", name: "definicion", schema } },
|
background: true, // <--- Ahora sí, activamos el modo background
|
||||||
metadata: {
|
metadata: {
|
||||||
usuario: /* user.email ?? user.id ??*/ "unknown",
|
tabla: "asignatura_mensajes_ia", // El webhook usará esto para saber dónde hacer el UPDATE
|
||||||
plan_estudio_id: row.plan_estudio_id,
|
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: [
|
input: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content:
|
content: isStructured
|
||||||
`Eres un asistente que ayuda a mejorar este plan de estudio: ${
|
? `Asistente de asignatura. Datos: ${JSON.stringify(asignatura)}`
|
||||||
JSON.stringify(planForPrompt)
|
: "Eres un experto en diseño curricular."
|
||||||
}. ` +
|
|
||||||
`Si te hacen una pregunta que no tiene nada que ver con el plan de estudio, responde con un refusal.`,
|
|
||||||
},
|
},
|
||||||
{ role: "user", content: prompt },
|
{ role: "user", content: body.content },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let parsed: any = null;
|
if (!aiResult.ok) {
|
||||||
try {
|
throw new HttpError(500, "openai_error", "No se pudo encolar la respuesta");
|
||||||
parsed = JSON.parse(resp.output_text ?? "null");
|
}
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
|
// 5. Responder al cliente de inmediato
|
||||||
return withCors(jsonResponse({
|
return withCors(jsonResponse({
|
||||||
ok: true,
|
ok: true,
|
||||||
openai_response_id: resp.id,
|
mensaje_id: mensajeInsertado.id,
|
||||||
data: parsed,
|
openai_response_id: aiResult.responseId
|
||||||
raw: resp.output_text ?? null,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return withCors(handleErr(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
|
* Unknown routes
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export const corsHeaders: Record<string, string> = {
|
export const corsHeaders: Record<string, string> = {
|
||||||
"access-control-allow-origin": "*",
|
"access-control-allow-origin": "*",
|
||||||
"access-control-allow-headers":
|
"Access-Control-Allow-Headers":
|
||||||
"authorization, x-client-info, apikey, content-type",
|
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform",
|
||||||
"access-control-allow-methods": "GET,POST,OPTIONS",
|
"access-control-allow-methods": "GET,POST,OPTIONS,DELETE",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function withCors(res: Response) {
|
export function withCors(res: Response) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HttpError } from "./errors.ts";
|
|||||||
|
|
||||||
export function pickSchemaFields(
|
export function pickSchemaFields(
|
||||||
definicion: any,
|
definicion: any,
|
||||||
campos?: string[],
|
campos: string[],
|
||||||
) {
|
) {
|
||||||
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
||||||
return definicion;
|
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.",
|
"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
|
// Si piden campos, filtramos propiedades/required a esos campos
|
||||||
if (Array.isArray(campos) && campos.length > 0) {
|
const entries = Object.entries(out.properties).filter(([k]) =>
|
||||||
const entries = Object.entries(out.properties).filter(([k]) =>
|
campos.includes(k)
|
||||||
campos.includes(k)
|
);
|
||||||
);
|
out.properties = Object.fromEntries(entries);
|
||||||
out.properties = Object.fromEntries(entries);
|
if (Array.isArray(out.required)) {
|
||||||
if (Array.isArray(out.required)) {
|
out.required = out.required.filter((k: string) => campos.includes(k));
|
||||||
out.required = out.required.filter((k: string) => campos.includes(k));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Siempre agregamos ai-message
|
// Siempre agregamos ai-message
|
||||||
@@ -42,6 +45,49 @@ export function pickSchemaFields(
|
|||||||
|
|
||||||
return out;
|
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) {
|
export function safePlanForPrompt(plan: any) {
|
||||||
const copy = structuredClone(plan);
|
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,
|
type StructuredResponseOptions,
|
||||||
} from "../_shared/openai-service.ts";
|
} 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 = {
|
export type DataAsignaturaSugerida = {
|
||||||
nombre: Tables<"asignaturas">["nombre"];
|
nombre: Tables<"asignaturas">["nombre"];
|
||||||
codigo?: Tables<"asignaturas">["codigo"];
|
codigo?: Tables<"asignaturas">["codigo"];
|
||||||
@@ -160,6 +167,11 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|||||||
SERVICE_ROLE_KEY,
|
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
|
const { data: plan, error: planError } = await supabaseService
|
||||||
.from("planes_estudio")
|
.from("planes_estudio")
|
||||||
.select("id,nombre,nivel,tipo_ciclo,numero_ciclos,datos")
|
.select("id,nombre,nivel,tipo_ciclo,numero_ciclos,datos")
|
||||||
@@ -276,7 +288,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options: StructuredResponseOptions = {
|
const options: StructuredResponseOptions = {
|
||||||
model: "gpt-5-mini",
|
model: GENERATE_SUBJECT_SUGGESTIONS_MODELO,
|
||||||
input: [
|
input: [
|
||||||
{ role: "system", content: systemPrompt },
|
{ 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(typeof data.output?.resumen === "string");
|
||||||
assert(data.output.resumen.length > 0);
|
assert(data.output.resumen.length > 0);
|
||||||
|
|
||||||
// opcional: valida que registró upload(s) a storage
|
// opcional: valida que registró upload(s) a OpenAI files
|
||||||
assert(Array.isArray(data.references?.uploadedToStorage));
|
assert(Array.isArray(data.references?.openaiFileIds));
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -160,6 +160,6 @@ Deno.test({
|
|||||||
assert(typeof data.output?.resumen === "string");
|
assert(typeof data.output?.resumen === "string");
|
||||||
assert(data.output.resumen.length > 0);
|
assert(data.output.resumen.length > 0);
|
||||||
|
|
||||||
// opcional: valida que registró upload(s) a storage
|
// opcional: valida que registró upload(s) a OpenAI files
|
||||||
assert(Array.isArray(data.references?.uploadedToStorage));
|
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;
|
SET row_security = off;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "pgsodium";
|
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";
|
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 (
|
CREATE TYPE "public"."estado_tarea_revision" AS ENUM (
|
||||||
'PENDIENTE',
|
'PENDIENTE',
|
||||||
'COMPLETADA',
|
'COMPLETADA',
|
||||||
@@ -151,7 +187,9 @@ CREATE TYPE "public"."tipo_cambio" AS ENUM (
|
|||||||
'ACTUALIZACION_CAMPO',
|
'ACTUALIZACION_CAMPO',
|
||||||
'ACTUALIZACION_MAPA',
|
'ACTUALIZACION_MAPA',
|
||||||
'TRANSICION_ESTADO',
|
'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";
|
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"
|
CREATE OR REPLACE FUNCTION "public"."set_actualizado_en"() RETURNS "trigger"
|
||||||
LANGUAGE "plpgsql"
|
LANGUAGE "plpgsql"
|
||||||
AS $$
|
AS $$
|
||||||
@@ -305,12 +505,10 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
|||||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||||
"plan_estudio_id" "uuid" NOT NULL,
|
"plan_estudio_id" "uuid" NOT NULL,
|
||||||
"estructura_id" "uuid",
|
"estructura_id" "uuid",
|
||||||
"facultad_propietaria_id" "uuid",
|
|
||||||
"codigo" "text",
|
"codigo" "text",
|
||||||
"nombre" "text" NOT NULL,
|
"nombre" "text" NOT NULL,
|
||||||
"tipo" "public"."tipo_asignatura" DEFAULT 'OBLIGATORIA'::"public"."tipo_asignatura" NOT NULL,
|
"tipo" "public"."tipo_asignatura" DEFAULT 'OBLIGATORIA'::"public"."tipo_asignatura" NOT NULL,
|
||||||
"creditos" numeric NOT NULL,
|
"creditos" numeric NOT NULL,
|
||||||
"horas_semana" integer,
|
|
||||||
"numero_ciclo" integer,
|
"numero_ciclo" integer,
|
||||||
"linea_plan_id" "uuid",
|
"linea_plan_id" "uuid",
|
||||||
"orden_celda" integer,
|
"orden_celda" integer,
|
||||||
@@ -322,9 +520,14 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
|||||||
"actualizado_por" "uuid",
|
"actualizado_por" "uuid",
|
||||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||||
"actualizado_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_ciclo_chk" CHECK ((("numero_ciclo" IS NULL) OR ("numero_ciclo" > 0))),
|
||||||
CONSTRAINT "asignaturas_creditos_check" CHECK (("creditos" >= (0)::numeric)),
|
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)))
|
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",
|
"campo" "text",
|
||||||
"valor_anterior" "jsonb",
|
"valor_anterior" "jsonb",
|
||||||
"valor_nuevo" "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";
|
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" (
|
CREATE TABLE IF NOT EXISTS "public"."estados_plan" (
|
||||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||||
"clave" "text" 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,
|
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||||
"actualizado_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,
|
"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))
|
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";
|
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" (
|
CREATE TABLE IF NOT EXISTS "public"."responsables_asignatura" (
|
||||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||||
"asignatura_id" "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"
|
ALTER TABLE ONLY "public"."estados_plan"
|
||||||
ADD CONSTRAINT "estados_plan_clave_key" UNIQUE ("clave");
|
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");
|
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"();
|
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"
|
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;
|
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"
|
ALTER TABLE ONLY "public"."carreras"
|
||||||
ADD CONSTRAINT "carreras_facultad_id_fkey" FOREIGN KEY ("facultad_id") REFERENCES "public"."facultades"("id") ON DELETE RESTRICT;
|
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"
|
ALTER TABLE ONLY "public"."interacciones_ia"
|
||||||
ADD CONSTRAINT "interacciones_ia_asignatura_id_fkey" FOREIGN KEY ("asignatura_id") REFERENCES "public"."asignaturas"("id") ON DELETE CASCADE;
|
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 "postgres";
|
||||||
GRANT USAGE ON SCHEMA "public" TO "anon";
|
GRANT USAGE ON SCHEMA "public" TO "anon";
|
||||||
GRANT USAGE ON SCHEMA "public" TO "authenticated";
|
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 "anon";
|
||||||
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "authenticated";
|
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"."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 "anon";
|
||||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "authenticated";
|
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "authenticated";
|
||||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "service_role";
|
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 "anon";
|
||||||
GRANT ALL ON TABLE "public"."estados_plan" TO "authenticated";
|
GRANT ALL ON TABLE "public"."estados_plan" TO "authenticated";
|
||||||
GRANT ALL ON TABLE "public"."estados_plan" TO "service_role";
|
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 "anon";
|
||||||
GRANT ALL ON TABLE "public"."responsables_asignatura" TO "authenticated";
|
GRANT ALL ON TABLE "public"."responsables_asignatura" TO "authenticated";
|
||||||
GRANT ALL ON TABLE "public"."responsables_asignatura" TO "service_role";
|
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