Compare commits
299 Commits
0d00ce1dc1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a9465edf2 | |||
| f6a9837b47 | |||
| aa4f989615 | |||
| 3d1aafff22 | |||
| 397f495601 | |||
| 6227c05e9d | |||
| 08a73213bb | |||
| dbfe082f0b | |||
| 542f2bc61a | |||
| 220b830aa8 | |||
| 77a5b3ad06 | |||
| 9ce5f0a471 | |||
| 81b8e444d0 | |||
| e9e7ebfb57 | |||
| cca45c38bc | |||
| 7a1ec8799b | |||
| d378c43821 | |||
| 93c4e46e62 | |||
| 28c4732f44 | |||
| 1dea287287 | |||
| 6532ae5c31 | |||
| ad6fbcc220 | |||
| 877761c318 | |||
| 1b8bfcc139 | |||
| 873e1c2612 | |||
| af7a913145 | |||
| 1048926f9d | |||
| c4a13ebd1d | |||
| 32ba862912 | |||
| 94ccf73176 | |||
| 8095565224 | |||
| 9245bbf801 | |||
| 7d1d64ded0 | |||
| eb656e8e75 | |||
| 75d9841db2 | |||
| 20b0a030e7 | |||
| 51dbae221a | |||
| dbd3364bcf | |||
| 4dd545d2da | |||
| fe2ab72da5 | |||
| bd7016e598 | |||
| af4dee9564 | |||
| 5ca47c19be | |||
| b8dd450e9b | |||
| 6d9bc660e0 | |||
| 8105c64add | |||
| 9eaabcd1f4 | |||
| 55335e1d77 | |||
| 0df513d4dc | |||
| e6e19fc190 | |||
| 735202096c | |||
| 808c0770d8 | |||
| b53db8eaff | |||
| b8b8b8119b | |||
| 6800594d35 | |||
| e0ea71ebc9 | |||
| 6e4265bdf8 | |||
| 375db1e7ce | |||
| 147df4ed66 | |||
| b29d9b1262 | |||
| b43b047485 | |||
| a9e010f64d | |||
| 24f090be37 | |||
| fb9602b849 | |||
| 2373eccf51 | |||
| 1e0ed7347a | |||
| 80ae3cd85a | |||
| 2b36228789 | |||
| d349feafd8 | |||
| 0746c9e799 | |||
| 0e4ecd26b9 | |||
| 01e98b445f | |||
| 2a7a808eab | |||
| 0151277a3e | |||
| 86563827d8 | |||
| 4b34ce6a0f | |||
| 2812f73043 | |||
| efdc9956f7 | |||
| fef340e8e8 | |||
| 4c3724f06c | |||
| 31a4278f12 | |||
| 260cc74111 | |||
| 5ffa735a7c | |||
| 6a56356c39 | |||
| b766cbf125 | |||
| 74b89e4eb5 | |||
| c620959b3b | |||
| 93be27a373 | |||
| c2eeda50a4 | |||
| 54e848269d | |||
| 28b3df1fbd | |||
| 97bbef7a0c | |||
| 357c244f2a | |||
| 55fd3f3886 | |||
| a00ccf838d | |||
| b0a6014a03 | |||
| 631e85b3d6 | |||
| 0a1f61f587 | |||
| 76dcf24991 | |||
| 0439a30a3f | |||
| dfc9fb162a | |||
| c40a4f3489 | |||
| 3c81256c87 | |||
| 82014cdbe2 | |||
| b4177e823f | |||
| 2888a41312 | |||
| c55d2da3cb | |||
| 03b94b3460 | |||
| 58e4d7f0db | |||
| c1abc0a250 | |||
| e8665f7738 | |||
| 2b22d80706 | |||
| 026a9b7255 | |||
| a92cd1cf45 | |||
| d72ea28860 | |||
| fb7a011148 | |||
| cc6dca4453 | |||
| e636ae76b5 | |||
| fa1f7e6c7c | |||
| 90b492c950 | |||
| 12dfcc86e5 | |||
| 37a016481e | |||
| 18acc2b010 | |||
| d110c4644d | |||
| 667dbc7d2d | |||
| 2dbe00a670 | |||
| b628adc99a | |||
| 6a7550a0d5 | |||
| abd7e62998 | |||
| 115c213135 | |||
| 3ee2e08974 | |||
| a1d03356c2 | |||
| 9be3e1ef13 | |||
| d92308f946 | |||
| ff2cb58271 | |||
| 92dd0e5e9e | |||
| 13197cc4fd | |||
| 241c46a1b8 | |||
| ff655e5598 | |||
| 7d31c6893f | |||
| 3a990147e1 | |||
| 8d4c661c22 | |||
| 4d67eb6b51 | |||
| 47e171a5b8 | |||
| 6301d52990 | |||
| 96ea054a8b | |||
| f11256633a | |||
| 9e5afed71e | |||
| a256a688f8 | |||
| 0d7c737baf | |||
| 481a126093 | |||
| 32c8a8814f | |||
| 8409001587 | |||
| 0343fb9d01 | |||
| 8dc1b8dee1 | |||
| f038ad4914 | |||
| caa340ef22 | |||
| a430811a1e | |||
| 3e6b889f6c | |||
| f2337b29b0 | |||
| b63857d38f | |||
| 347f932a21 | |||
| 496171cdea | |||
| 2ff1a5e8a9 | |||
| 0f5803d648 | |||
| 3959b97aa1 | |||
| b04961a944 | |||
| f81200e1a6 | |||
| c2a7eea20a | |||
| ffa9a41846 | |||
| 7152f91dfc | |||
| ec927111b3 | |||
| 0c2310d0d7 | |||
| 13645dc00a | |||
| 1b715baff6 | |||
| 5958ec15d2 | |||
| 2823c81052 | |||
| e72951d32d | |||
| f53273f3e5 | |||
| 16092e94a3 | |||
| 244bf9bd38 | |||
| 275e6f8917 | |||
| ed4891594b | |||
| 5fac762678 | |||
| c8c1b72964 | |||
| 749ec7a962 | |||
| d844698ffc | |||
| d7e232eec7 | |||
| 994fb3c7bc | |||
| 189225f369 | |||
| 7543afb56f | |||
| 5eb34205e0 | |||
| 3759f7673a | |||
| d4ceac77a9 | |||
| 99a6e28f42 | |||
| 606db3362c | |||
| d7312d2ab3 | |||
| c88bf044d0 | |||
| c12e2895a8 | |||
| 4944f1d31a | |||
| b2307759bf | |||
| 288278ea6c | |||
| 39f5b5d338 | |||
| 527f3aa56f | |||
| 9263587f0b | |||
| 2552a796a6 | |||
| 44919f074d | |||
| 8aeb1d0a64 | |||
| 824db85305 | |||
| 5fae2165b0 | |||
| 738d3970e3 | |||
| 009e65d327 | |||
| e43927f1b1 | |||
| e906fe258c | |||
| 44c86cfa13 | |||
| e81a8bd6a5 | |||
| c7bd107297 | |||
| 7ee9944ab5 | |||
| dcaeeb72dd | |||
| 26c6ad7c20 | |||
| 49b1a9805b | |||
| ef706dcf17 | |||
| d029fa23e7 | |||
| f816e31b63 | |||
| 38e1488c8c | |||
| a028fd459f | |||
| db3355f8ca | |||
| 5c27e61ce4 | |||
| 4b0b8e6a95 | |||
| ef42d74fea | |||
| feaad85fda | |||
| 21469b3b4c | |||
| 329faac742 | |||
| fed348e214 | |||
| 63e0ea7fbf | |||
| 6dc3167bc4 | |||
| c28274334a | |||
| a92fc6ac26 | |||
| 5a23be9961 | |||
| e64c0de529 | |||
| 34162c6038 | |||
| e7d7f553a3 | |||
| 0a4fc51bf7 | |||
| af945cfa69 | |||
| afd10eaaf7 | |||
| 13cf57505f | |||
| 9d25203ef6 | |||
| 7a81e448f4 | |||
| d724bda6f5 | |||
| 3d407c5b91 | |||
| 2d13995d5d | |||
| b583f4c0fd | |||
| b7720496cf | |||
| febcce85bb | |||
| a70f0c52a9 | |||
| f441976a7a | |||
| cfbd0cebbb | |||
| f29465958a | |||
| 334800fab7 | |||
| 9981b30260 | |||
| 2ff587d639 | |||
| 48ab986cb8 | |||
| 24b6338074 | |||
| b1d431a683 | |||
| ce5360a5c2 | |||
| 93c7f11621 | |||
| a9b77cd792 | |||
| 4365303c49 | |||
| e68c876944 | |||
| 53d9ab2876 | |||
| ada4f3254c | |||
| e78863a636 | |||
| e8a0f1d082 | |||
| 90cfad6f76 | |||
| 64362f2e31 | |||
| 7e064cb615 | |||
| bdd4921795 | |||
| 4615da4223 | |||
| 484db49654 | |||
| 0b72a227f1 | |||
| 801e23a89c | |||
| 834e884c90 | |||
| e1410c65ec | |||
| ce9c0cf3e6 | |||
| a3a7fd0de8 | |||
| e7acf50b82 | |||
| c33089726c | |||
| 7755246e8b | |||
| 12736b7f1d | |||
| 5c9fde1a9e | |||
| 7f9226ce38 | |||
| f1563b4cbe | |||
| 00f0bddf1c | |||
| 1ca861c51d | |||
| 42a437afa3 | |||
| 7660d2c3a0 | |||
| e5657d6b0f | |||
| b1a90858dc | |||
| 132a51133b |
@@ -0,0 +1,25 @@
|
|||||||
|
name: Deploy Function
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'supabase/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_DESARROLLO_ACCESS_TOKEN }}
|
||||||
|
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_DESARROLLO_PROJECT_ID }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: supabase/setup-cli@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Deploy edge function
|
||||||
|
run: npx supabase@beta functions deploy --project-ref $SUPABASE_PROJECT_ID --use-api
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Deploy Migrations to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'supabase/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_DESARROLLO_ACCESS_TOKEN }}
|
||||||
|
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_DESARROLLO_PROJECT_ID }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: supabase/setup-cli@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- run: supabase link --project-ref $SUPABASE_PROJECT_ID
|
||||||
|
- run: supabase db push --debug --yes
|
||||||
+2
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- 'supabase/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: Deploy Migrations to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'supabase/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||||
|
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: supabase/setup-cli@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- run: supabase link --project-ref $SUPABASE_PROJECT_ID
|
||||||
|
- run: supabase db push --debug --yes
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Update database types
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- supabase/migrations/*.sql
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_DESARROLLO_ACCESS_TOKEN }}
|
||||||
|
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_DESARROLLO_PROJECT_ID }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: supabase/setup-cli@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
- name: Link supabase project
|
||||||
|
run: supabase link --project-ref $SUPABASE_PROJECT_ID
|
||||||
|
- name: Generar seed de supabase
|
||||||
|
run: supabase db dump -f supabase/seed.sql --data-only
|
||||||
|
- name: Generar tipos de supabase
|
||||||
|
run: supabase gen types typescript --schema public > supabase/functions/_shared/database.types.ts
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: git_status
|
||||||
|
run: |
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "changed=true" >> $GITEA_OUTPUT
|
||||||
|
else
|
||||||
|
echo "changed=false" >> $GITEA_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
if: steps.git_status.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "supabot"
|
||||||
|
git config user.email "info.ingenieria@lasalle.mx"
|
||||||
|
git add supabase/seed.sql
|
||||||
|
git add supabase/functions/_shared/database.types.ts
|
||||||
|
git commit -m "chore: update database seed & typescript types"
|
||||||
|
|
||||||
|
- name: Push changes
|
||||||
|
if: steps.git_status.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git pull
|
||||||
|
git push
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
supabase/snippets
|
||||||
@@ -13,3 +13,5 @@ bun run index.ts
|
|||||||
```
|
```
|
||||||
|
|
||||||
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
|
|
||||||
|
<!--Comentario: 32014588721e71ba6ae5dc401668634c1bad3b0c0bb117fdfe97520d445962c8 -->
|
||||||
@@ -2,11 +2,16 @@
|
|||||||
"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",
|
||||||
"@zod/zod": "jsr:@zod/zod@^4.3.5",
|
"@zod/zod": "jsr:@zod/zod@^4.3.5",
|
||||||
"@david/dax": "jsr:@david/dax@^0.44.2"
|
"@david/dax": "jsr:@david/dax@^0.44.2",
|
||||||
|
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||||
|
"openai": "npm:openai@6.16.0",
|
||||||
|
"openai/helpers/zod": "npm:openai@6.16.0/helpers/zod",
|
||||||
|
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
||||||
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@
|
|||||||
"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": "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",
|
||||||
|
"jsr:@std/dotenv@*": "0.225.6",
|
||||||
"jsr:@std/fmt@1": "1.0.9",
|
"jsr:@std/fmt@1": "1.0.9",
|
||||||
"jsr:@std/fs@1": "1.0.22",
|
"jsr:@std/fs@1": "1.0.22",
|
||||||
"jsr:@std/fs@^1.0.20": "1.0.22",
|
"jsr:@std/fs@^1.0.20": "1.0.22",
|
||||||
@@ -14,12 +17,20 @@
|
|||||||
"jsr:@std/io@0.225": "0.225.3",
|
"jsr:@std/io@0.225": "0.225.3",
|
||||||
"jsr:@std/path@1": "1.1.4",
|
"jsr:@std/path@1": "1.1.4",
|
||||||
"jsr:@std/path@^1.1.4": "1.1.4",
|
"jsr:@std/path@^1.1.4": "1.1.4",
|
||||||
|
"jsr:@supabase/functions-js@2": "2.95.3",
|
||||||
"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.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:carbone-sdk@^1.6.0": "1.6.0",
|
||||||
|
"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:openai@^6.16.0": "6.16.0",
|
"npm:jwt-decode@4": "4.0.0",
|
||||||
"npm:supabase@^2.72.6": "2.72.6",
|
"npm:openai@6.16.0": "6.16.0_zod@3.25.76",
|
||||||
|
"npm:openai@^4.52.5": "4.104.0_zod@3.25.76",
|
||||||
|
"npm:openai@^6.16.0": "6.16.0_zod@3.25.76",
|
||||||
|
"npm:supabase@2.75.0": "2.75.0",
|
||||||
"npm:zod@3": "3.25.76"
|
"npm:zod@3": "3.25.76"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
@@ -48,6 +59,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": [
|
||||||
@@ -57,6 +71,9 @@
|
|||||||
"@std/bytes@1.0.6": {
|
"@std/bytes@1.0.6": {
|
||||||
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
||||||
},
|
},
|
||||||
|
"@std/dotenv@0.225.6": {
|
||||||
|
"integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b"
|
||||||
|
},
|
||||||
"@std/fmt@1.0.9": {
|
"@std/fmt@1.0.9": {
|
||||||
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
||||||
},
|
},
|
||||||
@@ -82,6 +99,12 @@
|
|||||||
"jsr:@std/internal"
|
"jsr:@std/internal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@supabase/functions-js@2.95.3": {
|
||||||
|
"integrity": "cedb60df283f2d9dabf39655a8829f2c99d173e5e11a867c46e6987c59a1d582",
|
||||||
|
"dependencies": [
|
||||||
|
"npm:openai@^4.52.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@zod/zod@4.3.5": {
|
"@zod/zod@4.3.5": {
|
||||||
"integrity": "3d14553f025d6e3d1a836b9c56e366bbce27d942301e8c7c1ac4c365df5f8085"
|
"integrity": "3d14553f025d6e3d1a836b9c56e366bbce27d942301e8c7c1ac4c365df5f8085"
|
||||||
}
|
}
|
||||||
@@ -89,57 +112,33 @@
|
|||||||
"npm": {
|
"npm": {
|
||||||
"@deno/darwin-arm64@2.6.4": {
|
"@deno/darwin-arm64@2.6.4": {
|
||||||
"integrity": "sha512-5pocuV2IVWYUzomXze+bDSO86uX1WPxUn82BS7BrLI3ix8Eq7l8DhvYgz064w7rwCM4CiLLSc6MAQpAI+0gUag==",
|
"integrity": "sha512-5pocuV2IVWYUzomXze+bDSO86uX1WPxUn82BS7BrLI3ix8Eq7l8DhvYgz064w7rwCM4CiLLSc6MAQpAI+0gUag==",
|
||||||
"os": [
|
"os": ["darwin"],
|
||||||
"darwin"
|
"cpu": ["arm64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/darwin-x64@2.6.4": {
|
"@deno/darwin-x64@2.6.4": {
|
||||||
"integrity": "sha512-tX7Fz47HcoXTYR4wN2WzXjqJtEPTftzjEknCYkZa8D4BzvpN7Zo1ra3Jd6bWPN9+T6mgTL6YMPMMSigXddIUcA==",
|
"integrity": "sha512-tX7Fz47HcoXTYR4wN2WzXjqJtEPTftzjEknCYkZa8D4BzvpN7Zo1ra3Jd6bWPN9+T6mgTL6YMPMMSigXddIUcA==",
|
||||||
"os": [
|
"os": ["darwin"],
|
||||||
"darwin"
|
"cpu": ["x64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/linux-arm64-glibc@2.6.4": {
|
"@deno/linux-arm64-glibc@2.6.4": {
|
||||||
"integrity": "sha512-wY4hjjVLq5idClWj6e1de9IUZ0QZ1rxo16o0dwCJpMi+w1lolxtYxGF4y7pkcxAdZOpMGsXZuYMPq1Z2n51esA==",
|
"integrity": "sha512-wY4hjjVLq5idClWj6e1de9IUZ0QZ1rxo16o0dwCJpMi+w1lolxtYxGF4y7pkcxAdZOpMGsXZuYMPq1Z2n51esA==",
|
||||||
"os": [
|
"os": ["linux"],
|
||||||
"linux"
|
"cpu": ["arm64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/linux-x64-glibc@2.6.4": {
|
"@deno/linux-x64-glibc@2.6.4": {
|
||||||
"integrity": "sha512-H9b0WJw+eWyC6GcMGCXxC/3lz2kVveyWyUrzGyQ8hJmtDJpWiDnCU2iPqEpEcwXHgAUFFyjnRaFwedwwv5ApdA==",
|
"integrity": "sha512-H9b0WJw+eWyC6GcMGCXxC/3lz2kVveyWyUrzGyQ8hJmtDJpWiDnCU2iPqEpEcwXHgAUFFyjnRaFwedwwv5ApdA==",
|
||||||
"os": [
|
"os": ["linux"],
|
||||||
"linux"
|
"cpu": ["x64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/win32-arm64@2.6.4": {
|
"@deno/win32-arm64@2.6.4": {
|
||||||
"integrity": "sha512-fMH1dx54ORbCTpMorGjDshwonIu6KfgfPC+Pq+14jeq/8604EIFXiA484p3Ov+ViZycndNHEPe8ZKPNd8xLjJA==",
|
"integrity": "sha512-fMH1dx54ORbCTpMorGjDshwonIu6KfgfPC+Pq+14jeq/8604EIFXiA484p3Ov+ViZycndNHEPe8ZKPNd8xLjJA==",
|
||||||
"os": [
|
"os": ["win32"],
|
||||||
"win32"
|
"cpu": ["arm64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/win32-x64@2.6.4": {
|
"@deno/win32-x64@2.6.4": {
|
||||||
"integrity": "sha512-hAxqNrBLHCINtqDgKcRiu8QtMbtdnHOVW3OFQjzA3gZEcpnGUK3JiF5ql++83SjuQH8ViZKGcdqkioLR7Bf14A==",
|
"integrity": "sha512-hAxqNrBLHCINtqDgKcRiu8QtMbtdnHOVW3OFQjzA3gZEcpnGUK3JiF5ql++83SjuQH8ViZKGcdqkioLR7Bf14A==",
|
||||||
"os": [
|
"os": ["win32"],
|
||||||
"win32"
|
"cpu": ["x64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@isaacs/fs-minipass@4.0.1": {
|
"@isaacs/fs-minipass@4.0.1": {
|
||||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
@@ -191,14 +190,24 @@
|
|||||||
"@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": [
|
||||||
"bun-types"
|
"bun-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/node@25.0.8": {
|
"@types/node-fetch@2.6.13": {
|
||||||
"integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
|
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/node",
|
||||||
|
"form-data@4.0.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@types/node@18.19.130": {
|
||||||
|
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"undici-types"
|
"undici-types"
|
||||||
]
|
]
|
||||||
@@ -212,9 +221,24 @@
|
|||||||
"@types/node"
|
"@types/node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"abort-controller@3.0.0": {
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"dependencies": [
|
||||||
|
"event-target-shim"
|
||||||
|
]
|
||||||
|
},
|
||||||
"agent-base@7.1.4": {
|
"agent-base@7.1.4": {
|
||||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
|
||||||
},
|
},
|
||||||
|
"agentkeepalive@4.6.0": {
|
||||||
|
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"humanize-ms"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"asynckit@0.4.0": {
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"bin-links@6.0.0": {
|
"bin-links@6.0.0": {
|
||||||
"integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==",
|
"integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -231,12 +255,36 @@
|
|||||||
"@types/node"
|
"@types/node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"call-bind-apply-helpers@1.0.2": {
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"es-errors",
|
||||||
|
"function-bind"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"carbone-sdk@1.6.0": {
|
||||||
|
"integrity": "sha512-fo/dPMwWnaCDK8sqwpkQbm7IiRpVKwRJu5gkVZj2gOQe6XyY6U6/N43c5bnA13GD9YxeI3WrWmj+rTbYlw8lEg==",
|
||||||
|
"dependencies": [
|
||||||
|
"form-data@4.0.0",
|
||||||
|
"kitten-cache",
|
||||||
|
"simple-get"
|
||||||
|
]
|
||||||
|
},
|
||||||
"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=="
|
||||||
},
|
},
|
||||||
|
"combined-stream@1.0.8": {
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": [
|
||||||
|
"delayed-stream"
|
||||||
|
]
|
||||||
|
},
|
||||||
"data-uri-to-buffer@4.0.1": {
|
"data-uri-to-buffer@4.0.1": {
|
||||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
|
||||||
},
|
},
|
||||||
@@ -246,6 +294,15 @@
|
|||||||
"ms"
|
"ms"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"decompress-response@6.0.0": {
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"mimic-response"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delayed-stream@1.0.0": {
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||||
|
},
|
||||||
"deno@2.6.4": {
|
"deno@2.6.4": {
|
||||||
"integrity": "sha512-/nHUtrwoqUsjVTBWGqz3SjAbtBSfqCLb38YdkwstrO5qpQd3RC20dxsGYPPANy9RqMFJMfDyARL7RtOcUfatpw==",
|
"integrity": "sha512-/nHUtrwoqUsjVTBWGqz3SjAbtBSfqCLb38YdkwstrO5qpQd3RC20dxsGYPPANy9RqMFJMfDyARL7RtOcUfatpw==",
|
||||||
"optionalDependencies": [
|
"optionalDependencies": [
|
||||||
@@ -259,11 +316,71 @@
|
|||||||
"scripts": true,
|
"scripts": true,
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
|
"dunder-proto@1.0.1": {
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dependencies": [
|
||||||
|
"call-bind-apply-helpers",
|
||||||
|
"es-errors",
|
||||||
|
"gopd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"es-define-property@1.0.1": {
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
|
||||||
|
},
|
||||||
|
"es-errors@1.3.0": {
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
|
||||||
|
},
|
||||||
|
"es-object-atoms@1.1.1": {
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dependencies": [
|
||||||
|
"es-errors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"es-set-tostringtag@2.1.0": {
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dependencies": [
|
||||||
|
"es-errors",
|
||||||
|
"get-intrinsic",
|
||||||
|
"has-tostringtag",
|
||||||
|
"hasown"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event-target-shim@5.0.1": {
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||||
|
},
|
||||||
"fetch-blob@3.2.0": {
|
"fetch-blob@3.2.0": {
|
||||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"node-domexception",
|
"node-domexception",
|
||||||
"web-streams-polyfill"
|
"web-streams-polyfill@3.3.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"form-data-encoder@1.7.2": {
|
||||||
|
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
|
||||||
|
},
|
||||||
|
"form-data@4.0.0": {
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": [
|
||||||
|
"asynckit",
|
||||||
|
"combined-stream",
|
||||||
|
"mime-types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"form-data@4.0.5": {
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"dependencies": [
|
||||||
|
"asynckit",
|
||||||
|
"combined-stream",
|
||||||
|
"es-set-tostringtag",
|
||||||
|
"hasown",
|
||||||
|
"mime-types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formdata-node@4.4.1": {
|
||||||
|
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"node-domexception",
|
||||||
|
"web-streams-polyfill@4.0.0-beta.3"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formdata-polyfill@4.0.10": {
|
"formdata-polyfill@4.0.10": {
|
||||||
@@ -272,6 +389,49 @@
|
|||||||
"fetch-blob"
|
"fetch-blob"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"function-bind@1.1.2": {
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
||||||
|
},
|
||||||
|
"get-intrinsic@1.3.0": {
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"call-bind-apply-helpers",
|
||||||
|
"es-define-property",
|
||||||
|
"es-errors",
|
||||||
|
"es-object-atoms",
|
||||||
|
"function-bind",
|
||||||
|
"get-proto",
|
||||||
|
"gopd",
|
||||||
|
"has-symbols",
|
||||||
|
"hasown",
|
||||||
|
"math-intrinsics"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get-proto@1.0.1": {
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dependencies": [
|
||||||
|
"dunder-proto",
|
||||||
|
"es-object-atoms"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gopd@1.2.0": {
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
|
||||||
|
},
|
||||||
|
"has-symbols@1.1.0": {
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
|
||||||
|
},
|
||||||
|
"has-tostringtag@1.0.2": {
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dependencies": [
|
||||||
|
"has-symbols"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hasown@2.0.2": {
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"function-bind"
|
||||||
|
]
|
||||||
|
},
|
||||||
"https-proxy-agent@7.0.6": {
|
"https-proxy-agent@7.0.6": {
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -279,12 +439,39 @@
|
|||||||
"debug"
|
"debug"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"humanize-ms@1.2.1": {
|
||||||
|
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"ms"
|
||||||
|
]
|
||||||
|
},
|
||||||
"iceberg-js@0.8.1": {
|
"iceberg-js@0.8.1": {
|
||||||
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="
|
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="
|
||||||
},
|
},
|
||||||
"imurmurhash@0.1.4": {
|
"imurmurhash@0.1.4": {
|
||||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
|
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
|
||||||
},
|
},
|
||||||
|
"jwt-decode@4.0.0": {
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||||
|
},
|
||||||
|
"kitten-cache@0.3.0": {
|
||||||
|
"integrity": "sha512-PFu7PEmbDSFehdsGXz7JtE1L5fFktV+apTcmyamWPo5IuDnFaTc5dT24JlrgB2RzYclLunimWUX5i3vBkPfv9w=="
|
||||||
|
},
|
||||||
|
"math-intrinsics@1.1.0": {
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
||||||
|
},
|
||||||
|
"mime-db@1.52.0": {
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||||
|
},
|
||||||
|
"mime-types@2.1.35": {
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": [
|
||||||
|
"mime-db"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mimic-response@3.1.0": {
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
|
||||||
|
},
|
||||||
"minipass@7.1.2": {
|
"minipass@7.1.2": {
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
||||||
},
|
},
|
||||||
@@ -301,6 +488,12 @@
|
|||||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
"deprecated": true
|
"deprecated": true
|
||||||
},
|
},
|
||||||
|
"node-fetch@2.7.0": {
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": [
|
||||||
|
"whatwg-url"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node-fetch@3.3.2": {
|
"node-fetch@3.3.2": {
|
||||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -312,8 +505,37 @@
|
|||||||
"npm-normalize-package-bin@5.0.0": {
|
"npm-normalize-package-bin@5.0.0": {
|
||||||
"integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="
|
"integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="
|
||||||
},
|
},
|
||||||
"openai@6.16.0": {
|
"once@1.4.0": {
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dependencies": [
|
||||||
|
"wrappy"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"openai@4.104.0_zod@3.25.76": {
|
||||||
|
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/node",
|
||||||
|
"@types/node-fetch",
|
||||||
|
"abort-controller",
|
||||||
|
"agentkeepalive",
|
||||||
|
"form-data-encoder",
|
||||||
|
"formdata-node",
|
||||||
|
"node-fetch@2.7.0",
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"openai@6.16.0_zod@3.25.76": {
|
||||||
"integrity": "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==",
|
"integrity": "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==",
|
||||||
|
"dependencies": [
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"proc-log@6.1.0": {
|
"proc-log@6.1.0": {
|
||||||
@@ -325,36 +547,67 @@
|
|||||||
"signal-exit@4.1.0": {
|
"signal-exit@4.1.0": {
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
|
||||||
},
|
},
|
||||||
"supabase@2.72.6": {
|
"simple-concat@1.0.1": {
|
||||||
"integrity": "sha512-axO1MkZgwMIxj78vKiGosnuXwzOaQq0Kz0I8jheNMwJH9dKGf3q/arivYT44z8u3t5FjGUxQWOuQoJetzcmm/A==",
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
|
||||||
|
},
|
||||||
|
"simple-get@4.0.1": {
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"dependencies": [
|
||||||
|
"decompress-response",
|
||||||
|
"once",
|
||||||
|
"simple-concat"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"supabase@2.75.0": {
|
||||||
|
"integrity": "sha512-Xaai8Wp7F03OMkOWSyS1OMMdk5DxkauJxcMMesj4mO6jDGEZpWBwgo/gIUb4hsckexmr/RXYdpDpTbUIMXahbw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"bin-links",
|
"bin-links",
|
||||||
"https-proxy-agent",
|
"https-proxy-agent",
|
||||||
"node-fetch",
|
"node-fetch@3.3.2",
|
||||||
"tar"
|
"tar"
|
||||||
],
|
],
|
||||||
"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",
|
||||||
"minipass",
|
"minipass",
|
||||||
"minizlib",
|
"minizlib",
|
||||||
"yallist"
|
"yallist"
|
||||||
]
|
],
|
||||||
|
"deprecated": true
|
||||||
|
},
|
||||||
|
"tr46@0.0.3": {
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
},
|
},
|
||||||
"tslib@2.8.1": {
|
"tslib@2.8.1": {
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
"undici-types@7.16.0": {
|
"undici-types@5.26.5": {
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
},
|
},
|
||||||
"web-streams-polyfill@3.3.3": {
|
"web-streams-polyfill@3.3.3": {
|
||||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
|
||||||
},
|
},
|
||||||
|
"web-streams-polyfill@4.0.0-beta.3": {
|
||||||
|
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="
|
||||||
|
},
|
||||||
|
"webidl-conversions@3.0.1": {
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"whatwg-url@5.0.0": {
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": [
|
||||||
|
"tr46",
|
||||||
|
"webidl-conversions"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wrappy@1.0.2": {
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
|
},
|
||||||
"write-file-atomic@7.0.0": {
|
"write-file-atomic@7.0.0": {
|
||||||
"integrity": "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==",
|
"integrity": "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -372,20 +625,44 @@
|
|||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.224.0/dotenv/mod.ts": "0180eaeedaaf88647318811cdaa418cc64dc51fb08354f91f5f480d0a1309f7d",
|
||||||
|
"https://deno.land/std@0.224.0/dotenv/parse.ts": "09977ff88dfd1f24f9973a338f0f91bbdb9307eb5ff6085446e7c423e4c7ba0c",
|
||||||
|
"https://deno.land/std@0.224.0/dotenv/stringify.ts": "275da322c409170160440836342eaa7cf012a1d11a7e700d8ca4e7f2f8aa4615",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4",
|
||||||
|
"https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e"
|
||||||
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@david/dax@~0.44.2",
|
"jsr:@david/dax@~0.44.2",
|
||||||
"jsr:@openai/openai@^6.16.0",
|
"jsr:@openai/openai@^6.16.0",
|
||||||
|
"jsr:@supabase/functions-js@2",
|
||||||
"jsr:@zod/zod@^4.3.5",
|
"jsr:@zod/zod@^4.3.5",
|
||||||
"jsr:@david/dax@~0.44.2"
|
"npm:@supabase/supabase-js@2",
|
||||||
|
"npm:openai@6.16.0"
|
||||||
],
|
],
|
||||||
"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:carbone-sdk@^1.6.0",
|
||||||
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+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
|
||||||
|
}
|
||||||
@@ -0,0 +1,772 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "76919cba",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import { load } from \"jsr:@std/dotenv\";\n",
|
||||||
|
"\n",
|
||||||
|
"const env = await load({\n",
|
||||||
|
" export: true,\n",
|
||||||
|
"});\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "20231839",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"type ListTemplatesParams = {\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" versionId?: string;\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" origin?: 0 | 1;\n",
|
||||||
|
" includeVersions?: boolean;\n",
|
||||||
|
" search?: string;\n",
|
||||||
|
" limit?: number;\n",
|
||||||
|
" cursor?: string;\n",
|
||||||
|
"};\n",
|
||||||
|
"\n",
|
||||||
|
"class CarboneClient {\n",
|
||||||
|
" listTemplates(params?: ListTemplatesParams) {}\n",
|
||||||
|
" listCategories() {}\n",
|
||||||
|
" listTags() {}\n",
|
||||||
|
" uploadTemplate(input: {\n",
|
||||||
|
" file?: Buffer;\n",
|
||||||
|
" filename?: string;\n",
|
||||||
|
" template?: string; // base64\n",
|
||||||
|
" versioning?: boolean;\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" name?: string;\n",
|
||||||
|
" comment?: string;\n",
|
||||||
|
" tags?: string[];\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" sample?: any[];\n",
|
||||||
|
" deployedAt?: number;\n",
|
||||||
|
" expireAt?: number;\n",
|
||||||
|
" }) {}\n",
|
||||||
|
" patchTemplate(id: string, body: {\n",
|
||||||
|
" name?: string;\n",
|
||||||
|
" comment?: string;\n",
|
||||||
|
" tags?: string[];\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" deployedAt?: number;\n",
|
||||||
|
" expireAt?: number;\n",
|
||||||
|
" }) {}\n",
|
||||||
|
" deleteTemplate(id: string) {}\n",
|
||||||
|
" downloadTemplate(id: string) {}\n",
|
||||||
|
" render(templateId: string, body: any, opts?: { download?: boolean }) {}\n",
|
||||||
|
" renderFromTemplate(body: any, opts?: { download?: boolean }) {}\n",
|
||||||
|
" downloadRender(renderId: string) {}\n",
|
||||||
|
"}\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "9950c1f7",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"const CARBONE_VERSION = \"5\";\n",
|
||||||
|
"\n",
|
||||||
|
"type CarboneSuccess<T> = {\n",
|
||||||
|
" success: true;\n",
|
||||||
|
" data: T;\n",
|
||||||
|
" hasMore?: boolean;\n",
|
||||||
|
" nextCursor?: string;\n",
|
||||||
|
"};\n",
|
||||||
|
"\n",
|
||||||
|
"type CarboneError = {\n",
|
||||||
|
" success: false;\n",
|
||||||
|
" error: string;\n",
|
||||||
|
"};\n",
|
||||||
|
"\n",
|
||||||
|
"type CarboneResponse<T> = CarboneSuccess<T> | CarboneError;\n",
|
||||||
|
"\n",
|
||||||
|
"export class CarboneClient {\n",
|
||||||
|
" constructor(\n",
|
||||||
|
" private baseUrl: string,\n",
|
||||||
|
" private apiToken: string,\n",
|
||||||
|
" ) {}\n",
|
||||||
|
"\n",
|
||||||
|
" private buildHeaders(extra?: Record<string, string>) {\n",
|
||||||
|
" return {\n",
|
||||||
|
" Authorization: `Bearer ${this.apiToken}`,\n",
|
||||||
|
" \"carbone-version\": CARBONE_VERSION,\n",
|
||||||
|
" ...extra,\n",
|
||||||
|
" };\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" private async parseResponse<T>(res: Response): Promise<T> {\n",
|
||||||
|
" const contentType = res.headers.get(\"content-type\") || \"\";\n",
|
||||||
|
"\n",
|
||||||
|
" if (!res.ok) {\n",
|
||||||
|
" let message = `Carbone request failed: ${res.status}`;\n",
|
||||||
|
" try {\n",
|
||||||
|
" const err = await res.json();\n",
|
||||||
|
" message = err?.error || err?.message || message;\n",
|
||||||
|
" } catch {}\n",
|
||||||
|
" throw new Error(message);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" if (contentType.includes(\"application/json\")) {\n",
|
||||||
|
" return res.json() as Promise<T>;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" return res as unknown as T;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" private toQuery(params?: Record<string, unknown>) {\n",
|
||||||
|
" const qs = new URLSearchParams();\n",
|
||||||
|
" if (!params) return \"\";\n",
|
||||||
|
" for (const [key, value] of Object.entries(params)) {\n",
|
||||||
|
" if (value === undefined || value === null) continue;\n",
|
||||||
|
" qs.set(key, String(value));\n",
|
||||||
|
" }\n",
|
||||||
|
" const s = qs.toString();\n",
|
||||||
|
" return s ? `?${s}` : \"\";\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async listTemplates(params?: {\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" versionId?: string;\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" origin?: 0 | 1;\n",
|
||||||
|
" includeVersions?: boolean;\n",
|
||||||
|
" search?: string;\n",
|
||||||
|
" limit?: number;\n",
|
||||||
|
" cursor?: string;\n",
|
||||||
|
" }) {\n",
|
||||||
|
" const res = await fetch(\n",
|
||||||
|
" `${this.baseUrl}/templates${this.toQuery(params)}`,\n",
|
||||||
|
" {\n",
|
||||||
|
" method: \"GET\",\n",
|
||||||
|
" headers: this.buildHeaders(),\n",
|
||||||
|
" },\n",
|
||||||
|
" );\n",
|
||||||
|
" return this.parseResponse<\n",
|
||||||
|
" CarboneResponse<\n",
|
||||||
|
" Array<{\n",
|
||||||
|
" id: string;\n",
|
||||||
|
" versionId: string;\n",
|
||||||
|
" deployedAt: number;\n",
|
||||||
|
" createdAt: number;\n",
|
||||||
|
" expireAt?: number;\n",
|
||||||
|
" size: number;\n",
|
||||||
|
" type: string;\n",
|
||||||
|
" name?: string;\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" comment?: string;\n",
|
||||||
|
" tags?: string[];\n",
|
||||||
|
" origin?: number;\n",
|
||||||
|
" }>\n",
|
||||||
|
" >\n",
|
||||||
|
" >(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async listCategories() {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/templates/categories`, {\n",
|
||||||
|
" method: \"GET\",\n",
|
||||||
|
" headers: this.buildHeaders(),\n",
|
||||||
|
" });\n",
|
||||||
|
" return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async listTags() {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/templates/tags`, {\n",
|
||||||
|
" method: \"GET\",\n",
|
||||||
|
" headers: this.buildHeaders(),\n",
|
||||||
|
" });\n",
|
||||||
|
" return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async uploadTemplateJson(body: {\n",
|
||||||
|
" template: string;\n",
|
||||||
|
" versioning?: boolean;\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" name?: string;\n",
|
||||||
|
" comment?: string;\n",
|
||||||
|
" tags?: string[];\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" sample?: any[];\n",
|
||||||
|
" deployedAt?: number;\n",
|
||||||
|
" expireAt?: number;\n",
|
||||||
|
" }) {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/template`, {\n",
|
||||||
|
" method: \"POST\",\n",
|
||||||
|
" headers: this.buildHeaders({\n",
|
||||||
|
" \"Content-Type\": \"application/json\",\n",
|
||||||
|
" }),\n",
|
||||||
|
" body: JSON.stringify(body),\n",
|
||||||
|
" });\n",
|
||||||
|
" return this.parseResponse<\n",
|
||||||
|
" CarboneResponse<{\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" versionId?: string;\n",
|
||||||
|
" templateId?: string;\n",
|
||||||
|
" type?: string;\n",
|
||||||
|
" size?: number;\n",
|
||||||
|
" createdAt?: number;\n",
|
||||||
|
" }>\n",
|
||||||
|
" >(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async uploadTemplateFile(input: {\n",
|
||||||
|
" file: Blob;\n",
|
||||||
|
" filename: string;\n",
|
||||||
|
" versioning?: boolean;\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" name?: string;\n",
|
||||||
|
" comment?: string;\n",
|
||||||
|
" tags?: string[];\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" sample?: any[];\n",
|
||||||
|
" deployedAt?: number;\n",
|
||||||
|
" expireAt?: number;\n",
|
||||||
|
" }) {\n",
|
||||||
|
" const form = new FormData();\n",
|
||||||
|
"\n",
|
||||||
|
" if (input.versioning !== undefined) {\n",
|
||||||
|
" form.append(\"versioning\", String(input.versioning));\n",
|
||||||
|
" }\n",
|
||||||
|
" if (input.id) form.append(\"id\", input.id);\n",
|
||||||
|
" if (input.name) form.append(\"name\", input.name);\n",
|
||||||
|
" if (input.comment) form.append(\"comment\", input.comment);\n",
|
||||||
|
" if (input.category) form.append(\"category\", input.category);\n",
|
||||||
|
" if (input.tags) {\n",
|
||||||
|
" for (const tag of input.tags) form.append(\"tags[]\", tag);\n",
|
||||||
|
" }\n",
|
||||||
|
" if (input.sample) form.append(\"sample\", JSON.stringify(input.sample));\n",
|
||||||
|
" if (input.deployedAt !== undefined) {\n",
|
||||||
|
" form.append(\"deployedAt\", String(input.deployedAt));\n",
|
||||||
|
" }\n",
|
||||||
|
" if (input.expireAt !== undefined) {\n",
|
||||||
|
" form.append(\"expireAt\", String(input.expireAt));\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" // template should be appended last\n",
|
||||||
|
" form.append(\"template\", input.file, input.filename);\n",
|
||||||
|
"\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/template`, {\n",
|
||||||
|
" method: \"POST\",\n",
|
||||||
|
" headers: this.buildHeaders(),\n",
|
||||||
|
" body: form,\n",
|
||||||
|
" });\n",
|
||||||
|
" return this.parseResponse<\n",
|
||||||
|
" CarboneResponse<{\n",
|
||||||
|
" id?: string;\n",
|
||||||
|
" versionId?: string;\n",
|
||||||
|
" templateId?: string;\n",
|
||||||
|
" type?: string;\n",
|
||||||
|
" size?: number;\n",
|
||||||
|
" createdAt?: number;\n",
|
||||||
|
" }>\n",
|
||||||
|
" >(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async patchTemplate(\n",
|
||||||
|
" id: string,\n",
|
||||||
|
" body: {\n",
|
||||||
|
" name?: string;\n",
|
||||||
|
" comment?: string;\n",
|
||||||
|
" tags?: string[];\n",
|
||||||
|
" category?: string;\n",
|
||||||
|
" deployedAt?: number;\n",
|
||||||
|
" expireAt?: number;\n",
|
||||||
|
" },\n",
|
||||||
|
" ) {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/template/${id}`, {\n",
|
||||||
|
" method: \"PATCH\",\n",
|
||||||
|
" headers: this.buildHeaders({\n",
|
||||||
|
" \"Content-Type\": \"application/json\",\n",
|
||||||
|
" }),\n",
|
||||||
|
" body: JSON.stringify(body),\n",
|
||||||
|
" });\n",
|
||||||
|
" return this.parseResponse<CarboneResponse<true>>(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async deleteTemplate(id: string) {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/template/${id}`, {\n",
|
||||||
|
" method: \"DELETE\",\n",
|
||||||
|
" headers: this.buildHeaders(),\n",
|
||||||
|
" });\n",
|
||||||
|
" return this.parseResponse<CarboneResponse<true>>(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async downloadTemplate(id: string) {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/template/${id}`, {\n",
|
||||||
|
" method: \"GET\",\n",
|
||||||
|
" headers: this.buildHeaders(),\n",
|
||||||
|
" });\n",
|
||||||
|
"\n",
|
||||||
|
" if (!res.ok) {\n",
|
||||||
|
" const err = await res.json().catch(() => null);\n",
|
||||||
|
" throw new Error(\n",
|
||||||
|
" err?.error || `Failed to download template: ${res.status}`,\n",
|
||||||
|
" );\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" return {\n",
|
||||||
|
" buffer: Buffer.from(await res.arrayBuffer()),\n",
|
||||||
|
" contentType: res.headers.get(\"content-type\"),\n",
|
||||||
|
" contentDisposition: res.headers.get(\"content-disposition\"),\n",
|
||||||
|
" };\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async render(\n",
|
||||||
|
" templateIdOrVersionId: string,\n",
|
||||||
|
" body: Record<string, unknown>,\n",
|
||||||
|
" opts?: { download?: boolean },\n",
|
||||||
|
" ) {\n",
|
||||||
|
" const query = opts?.download ? \"?download=true\" : \"\";\n",
|
||||||
|
" const res = await fetch(\n",
|
||||||
|
" `${this.baseUrl}/render/${templateIdOrVersionId}${query}`,\n",
|
||||||
|
" {\n",
|
||||||
|
" method: \"POST\",\n",
|
||||||
|
" headers: this.buildHeaders({\n",
|
||||||
|
" \"Content-Type\": \"application/json\",\n",
|
||||||
|
" }),\n",
|
||||||
|
" body: JSON.stringify(body),\n",
|
||||||
|
" },\n",
|
||||||
|
" );\n",
|
||||||
|
"\n",
|
||||||
|
" if (opts?.download) {\n",
|
||||||
|
" if (!res.ok) {\n",
|
||||||
|
" const err = await res.json().catch(() => null);\n",
|
||||||
|
" throw new Error(err?.error || `Render failed: ${res.status}`);\n",
|
||||||
|
" }\n",
|
||||||
|
" return {\n",
|
||||||
|
" buffer: Buffer.from(await res.arrayBuffer()),\n",
|
||||||
|
" contentType: res.headers.get(\"content-type\"),\n",
|
||||||
|
" contentDisposition: res.headers.get(\"content-disposition\"),\n",
|
||||||
|
" };\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async renderFromTemplate(\n",
|
||||||
|
" body: Record<string, unknown>,\n",
|
||||||
|
" opts?: { download?: boolean },\n",
|
||||||
|
" ) {\n",
|
||||||
|
" const query = opts?.download ? \"?download=true\" : \"\";\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/render/template${query}`, {\n",
|
||||||
|
" method: \"POST\",\n",
|
||||||
|
" headers: this.buildHeaders({\n",
|
||||||
|
" \"Content-Type\": \"application/json\",\n",
|
||||||
|
" }),\n",
|
||||||
|
" body: JSON.stringify(body),\n",
|
||||||
|
" });\n",
|
||||||
|
"\n",
|
||||||
|
" if (opts?.download) {\n",
|
||||||
|
" if (!res.ok) {\n",
|
||||||
|
" const err = await res.json().catch(() => null);\n",
|
||||||
|
" throw new Error(err?.error || `Render failed: ${res.status}`);\n",
|
||||||
|
" }\n",
|
||||||
|
" return {\n",
|
||||||
|
" buffer: Buffer.from(await res.arrayBuffer()),\n",
|
||||||
|
" contentType: res.headers.get(\"content-type\"),\n",
|
||||||
|
" contentDisposition: res.headers.get(\"content-disposition\"),\n",
|
||||||
|
" };\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" async downloadRender(renderId: string) {\n",
|
||||||
|
" const res = await fetch(`${this.baseUrl}/render/${renderId}`, {\n",
|
||||||
|
" method: \"GET\",\n",
|
||||||
|
" });\n",
|
||||||
|
"\n",
|
||||||
|
" if (!res.ok) {\n",
|
||||||
|
" const err = await res.json().catch(() => null);\n",
|
||||||
|
" throw new Error(err?.error || `Failed to download render: ${res.status}`);\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" return {\n",
|
||||||
|
" buffer: Buffer.from(await res.arrayBuffer()),\n",
|
||||||
|
" contentType: res.headers.get(\"content-type\"),\n",
|
||||||
|
" contentDisposition: res.headers.get(\"content-disposition\"),\n",
|
||||||
|
" };\n",
|
||||||
|
" }\n",
|
||||||
|
"}\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "95b24436",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"{\n",
|
||||||
|
" success: true,\n",
|
||||||
|
" data: [\n",
|
||||||
|
" {\n",
|
||||||
|
" versionId: \"1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6\",\n",
|
||||||
|
" id: \"1373945625988937236\",\n",
|
||||||
|
" deployedAt: 1773246290,\n",
|
||||||
|
" createdAt: 1773246316,\n",
|
||||||
|
" expireAt: 0,\n",
|
||||||
|
" size: 29008,\n",
|
||||||
|
" type: \"docx\",\n",
|
||||||
|
" name: \"Anexo 1\",\n",
|
||||||
|
" category: \"Planes de estudio\",\n",
|
||||||
|
" comment: \"Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n",
|
||||||
|
" tags: [],\n",
|
||||||
|
" origin: 1\n",
|
||||||
|
" },\n",
|
||||||
|
" {\n",
|
||||||
|
" versionId: \"caee5a7804647382f6df62ef92cc8f04c822b6a297479c5068e22d08def685d7\",\n",
|
||||||
|
" id: \"1373945108764607184\",\n",
|
||||||
|
" deployedAt: 1773246225,\n",
|
||||||
|
" createdAt: 1773246251,\n",
|
||||||
|
" expireAt: 0,\n",
|
||||||
|
" size: 29642,\n",
|
||||||
|
" type: \"xlsx\",\n",
|
||||||
|
" name: \"Anexo 2\",\n",
|
||||||
|
" category: \"\",\n",
|
||||||
|
" comment: \"\",\n",
|
||||||
|
" tags: [],\n",
|
||||||
|
" origin: 1\n",
|
||||||
|
" },\n",
|
||||||
|
" {\n",
|
||||||
|
" versionId: \"b0ab3c6fe90c73151fcae64d8509e406803381f3966aa88d30d8d30e2b1ffe8f\",\n",
|
||||||
|
" id: \"1373944894291796699\",\n",
|
||||||
|
" deployedAt: 1773246204,\n",
|
||||||
|
" createdAt: 1773246230,\n",
|
||||||
|
" expireAt: 0,\n",
|
||||||
|
" size: 23806,\n",
|
||||||
|
" type: \"docx\",\n",
|
||||||
|
" name: \"Anexo 3\",\n",
|
||||||
|
" category: \"\",\n",
|
||||||
|
" comment: \"\",\n",
|
||||||
|
" tags: [],\n",
|
||||||
|
" origin: 1\n",
|
||||||
|
" }\n",
|
||||||
|
" ],\n",
|
||||||
|
" hasMore: false\n",
|
||||||
|
"}\n",
|
||||||
|
"{ success: true, data: [ { name: \"Planes de estudio\" } ] }\n",
|
||||||
|
"{ success: true, data: [] }\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"const carbone = new CarboneClient(\n",
|
||||||
|
" \"https://carbone.lci.ulsa.mx\",\n",
|
||||||
|
" Deno.env.get(\"CARBONE_API_TOKEN\")!,\n",
|
||||||
|
");\n",
|
||||||
|
"\n",
|
||||||
|
"const templates = await carbone.listTemplates({ limit: 20 });\n",
|
||||||
|
"console.log(templates);\n",
|
||||||
|
"\n",
|
||||||
|
"const categories = await carbone.listCategories();\n",
|
||||||
|
"console.log(categories);\n",
|
||||||
|
"\n",
|
||||||
|
"const tags = await carbone.listTags();\n",
|
||||||
|
"console.log(tags);\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "8196fac4",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"const templateID = \"1373945625988937236\"; // Anexo 1 SEP RVOE Acuerdo 17-11-17\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"id": "caf8bc20",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{\n",
|
||||||
|
" success: \u001b[33mtrue\u001b[39m,\n",
|
||||||
|
" data: {\n",
|
||||||
|
" category: \u001b[32m\"Planes de estudio\"\u001b[39m,\n",
|
||||||
|
" comment: \u001b[32m\"Plantillaaa de anexo 1 SEP RVOE Acuerdo 17-11-17\"\u001b[39m,\n",
|
||||||
|
" versionId: \u001b[32m\"1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6\"\u001b[39m\n",
|
||||||
|
" }\n",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 6,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"await carbone.patchTemplate(templateID, {\n",
|
||||||
|
" category: \"Planes de estudio\",\n",
|
||||||
|
" comment: \"Plantillaaa de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n",
|
||||||
|
"});\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"id": "929883b9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"const result = await carbone.render(\n",
|
||||||
|
" templateID,\n",
|
||||||
|
" {\n",
|
||||||
|
" data: {\n",
|
||||||
|
" \"vigencia\": \"2026\",\n",
|
||||||
|
" \"area_de_estudio\":\n",
|
||||||
|
" \"Educación. Campo amplio: Formación docente y educación especial (CMPE 2016, INEGI)\",\n",
|
||||||
|
" \"perfil_de_egreso\":\n",
|
||||||
|
" \"Conocimientos:\\n- Conocer los fundamentos teóricos y normativos de la educación especial y la inclusión educativa, incluidas las leyes y políticas nacionales aplicables.\\n- Conocer modelos de atención educativa (modelo ecológico, enfoque por competencias, Diseño Universal para el Aprendizaje, respuesta a la intervención) y sus implicaciones para la práctica de sombra.\\n- Conocer características y necesidades educativas de estudiantes con discapacidad, trastornos del desarrollo y necesidades educativas especiales específicas.\\n\\nHabilidades:\\n- Diseñar y aplicar estrategias individualizadas de acompañamiento (sombra) para facilitar la participación y el aprendizaje en contextos educativos regulares.\\n- Realizar observaciones sistemáticas, registros conductuales y adaptaciones curriculares básicas en colaboración con el personal docente y familias.\\n- Colaborar en equipos interdisciplinarios y comunicarse efectivamente con docentes, familias y especialistas para articular apoyos.\\n- Aplicar procedimientos de evaluación formativa y ajustar estrategias de intervención basadas en evidencia.\\n\\nActitudes y valores:\\n- Actuar con ética profesional, confidencialidad y respeto hacia la dignidad, autonomía y derechos de las personas con discapacidad.\\n- Mantener una actitud reflexiva, crítica y de mejora continua sustentada en la evidencia y en el aprendizaje colaborativo.\\n- Mostrar compromiso por la inclusión, la equidad y la promoción de entornos accesibles y participativos.\",\n",
|
||||||
|
" \"diseno_curricular\": \"Rígido\",\n",
|
||||||
|
" \"perfil_de_ingreso\":\n",
|
||||||
|
" \"Conocimientos previos:\\n- Certificado de Bachillerato o equivalente con dominio básico de lectura, redacción y razonamiento lógico-matemático.\\n- Conocimientos introductorios en psicología, pedagogía o atención a la diversidad (deseable, no obligatorio).\\n\\nHabilidades y aptitudes:\\n- Habilidad comunicativa efectiva y disposición para el trabajo en equipo.\\n- Capacidad de observación, registro y análisis de comportamientos en contextos educativos.\\n- Actitud empática, paciente y respetuosa frente a la diversidad funcional y cultural.\\n\\nCondiciones personales:\\n- Disponibilidad para prácticas en centros educativos y visitas domiciliarias.\\n- Compromiso ético y disposición para formación continua y trabajo con poblaciones vulnerables.\",\n",
|
||||||
|
" \"curso_propedeutico\":\n",
|
||||||
|
" \"Se ofrecerá un curso propedéutico opcional dirigido a aspirantes que no cuenten con formación previa en pedagogía o atención a la diversidad. Características:\\n- Población: Aspirantes sin experiencia formal en educación o con antecedentes en áreas no afines.\\n- Duración: 8 semanas (40 horas totales), modalidad mixta (sincrónico y asincrónico).\\n- Contenidos: Fundamentos de psicología del desarrollo, nociones básicas de pedagogía inclusiva, comunicación efectiva y manejo básico de registros.\\n- Relación con el plan de estudios: Prepara al aspirante para cursar asignaturas iniciales de didáctica y atención a la diversidad; no substituye requisitos de admisión, pero fortalece rendimiento académico inicial.\\n- Criterios de aplicación: Se recomienda para el 30% de la cohorte cuya evaluación de diagnóstico inicial identifique brechas relevantes en competencias lectoras, comunicativas o conocimientos básicos en educación.\",\n",
|
||||||
|
" \"modalidad_educativa\": \"Mixta\",\n",
|
||||||
|
" \"antecedente_academico\":\n",
|
||||||
|
" \"Certificado de Bachillerato o equivalente. Preferentemente procedencia de áreas afines como Ciencias de la Educación, Psicología, Trabajo Social, Pedagogía o áreas de ciencias sociales relacionadas con atención a la infancia y discapacidad.\",\n",
|
||||||
|
" \"carga_horaria_a_la_semana\": 40,\n",
|
||||||
|
" \"programa_de_investigacion\": null,\n",
|
||||||
|
" \"clave_del_plan_de_estudios\": \"LLS-LESE-2026\",\n",
|
||||||
|
" \"duracion_del_ciclo_escolar\": \"16 semanas\",\n",
|
||||||
|
" \"fines_de_aprendizaje_o_formacion\":\n",
|
||||||
|
" \"El programa tiene por finalidad formar profesionales de la educación especializados en el acompañamiento tipo 'sombra', capaces de intervenir en contextos educativos regulares y de atención especializada mediante prácticas fundamentadas en la evidencia, la colaboración interprofesional y el respeto a los derechos de las personas con discapacidad. Los fines de aprendizaje se articulan en competencias que guían la formación:\\n- Analizar los marcos normativos, éticos y teóricos que sustentan la educación especial y la inclusión, con el fin de garantizar intervenciones respetuosas de los derechos y la dignidad de las personas con discapacidad.\\n- Identificar las características desarrollo-cognitivas, socioemocionales y comunicativas de estudiantes con diferentes tipos de discapacidad o necesidades educativas especiales, para adecuar estrategias de apoyo.\\n- Diseñar planes de acompañamiento individualizados basados en la evaluación formativa, que integren adaptaciones curriculares, apoyos conductuales y mediaciones pedagógicas pertinentes al contexto escolar.\\n- Implementar técnicas de observación, registro y evaluación del comportamiento y del progreso académico, utilizando instrumentos estandarizados y protocolos de registro confiables.\\n- Colaborar y coordinar con docentes titulares, equipos interdisciplinares y familias para articular apoyos, comunicando hallazgos y proponiendo ajustes en el entorno y la práctica pedagógica.\\n- Implementar estrategias de mediación y apoyo que promuevan la participación activa, la autonomía funcional y la inclusión social del alumno dentro del aula regular.\\n- Evaluar la eficacia de las intervenciones de sombra mediante indicadores cualitativos y cuantitativos, ajustando las acciones en ciclos de mejora continua.\\n- Promover entornos accesibles y diseñar ajustes razonables y recursos didácticos adaptados que favorezcan el aprendizaje y la interacción social.\\n- Fomentar actitudes profesionales basadas en la ética, la confidencialidad, el respeto a la diversidad cultural y la toma de decisiones centrada en la persona.\\n- Difundir prácticas basadas en evidencia y favorecer la investigación aplicada en contextos escolares para la mejora de modelos de acompañamiento y políticas inclusivas.\",\n",
|
||||||
|
" \"nivel_y_nombre_del_plan_de_estudios\":\n",
|
||||||
|
" \"Licenciatura en Educación Especial (orientación: Sombra)\",\n",
|
||||||
|
" \"nombre_autorizado_de_la_institucion\":\n",
|
||||||
|
" \"Universidad La Salle México\",\n",
|
||||||
|
" \"total_de_ciclos_del_plan_de_estudios\": \"9\",\n",
|
||||||
|
" \"sustento_teorico_del_modelo_curricular\":\n",
|
||||||
|
" \"Lista de pasos conceptuales previos:\\n- Revisar requisitoss normativos del Acuerdo 17/11/17 y la normativa educativa nacional aplicable.\\n- Estructurar campos obligatorios del Anexo 1 y verificar coherencia entre ingreso, perfil y fines de aprendizaje.\\n- Diseñar el marco curricular alineado con teorías de inclusión, UDL y prácticas basadas en evidencia.\\n- Determinar la operatividad administrativa y los mecanismos de evaluación periódica.\\n- Validar la carga horaria, duración de ciclos y practicum con estándares nacionales y de buenas prácticas.\\n\\nMarco teórico y justificación del modelo curricular:\\n El modelo curricular de la Licenciatura en Educación Especial con orientación en acompañamiento tipo sombra parte de la premisa de que la inclusión educativa es un derecho humano y una responsabilidad institucional que requiere profesionales formados de manera específica para acompañar procesos de aprendizaje y participación en contextos escolares regulares. Se sustenta en los siguientes referentes teóricos y conceptuales:\\n - Diseño Universal para el Aprendizaje (UDL): plantea principios para anticipar barreras y diseñar múltiples vías de acceso al currículo, favoreciendo la planificación de adaptaciones y apoyos que el profesional en sombra debe conocer y aplicar.\\n - Enfoque ecológico y bio-psicosocial: reconoce que las necesidades de los estudiantes emergen de la interacción entre individuo, familia, escuela y comunidad; por tanto, la intervención del profesional sombra se concibe como mediación contextual, no como sustitución del docente.\\n - Teorías socioculturales del aprendizaje (Vygotsky): sustentan la importancia del andamiaje y la zona de desarrollo próximo, herramientas clave para la intervención individualizada y para promover la autonomía del estudiante mediante mediación intencional.\\n - Modelos colaborativos y de co-intervención: el plan se apoya en evidencia sobre la efectividad de prácticas colaborativas entre docente titular, especialista y familia para lograr inclusión sostenible; el profesional sombra es formado para integrarse de forma articulada en estos equipos.\\n - Prácticas basadas en evidencia y evaluación formativa: el currículo incorpora métodos de observación sistemática, medición del progreso y diseño de intervenciones ajustadas a resultados, promoviendo la investigación aplicada y la cultura de evaluación.\\n - Perspectiva de derechos y normativa nacional: el modelo articula la formación con el marco legal mexicano en materia educativa y de discapacidad, así como con principios de ética profesional y confidencialidad.\\n\\n La estructura curricular combina contenidos conceptuales, metodológicos y prácticos; integra prácticas supervisadas desde el primer año, seminarios de reflexión profesional, y proyectos integradores que vinculan investigación aplicada con mejora institucional. La formación busca que el egresado maneje herramientas para la evaluación, diseño e implementación de apoyos que fomenten la inclusión, la participación y el desarrollo integral del estudiante.\",\n",
|
||||||
|
" \"justificacion_de_la_propuesta_curricular\":\n",
|
||||||
|
" \"Modalidad mixta: explicación de implementación tecnológica, autogestión y flexibilidad espacio-temporal.\\n- Plataforma tecnológica: El programa empleará un LMS institucional (por ejemplo, Moodle institucional o plataforma propietaria) que soporta contenidos asincrónicos (módulos temáticos, lecturas, foros), actividades evaluables y repositorio de recursos adaptativos. Además se dispondrá de herramientas para videoconferencia (Zoom/Teams) para sesiones sincrónicas, tutorías y supervisión de práctica.\\n- Autogestión del aprendizaje: Las actividades asincrónicas incluirán guías de estudio, rúbricas claras y recursos multimedia. Se promoverá la metacognición mediante diarios de aprendizaje y portafolios electrónicos que permitan evidenciar competencias profesionales.\\n- Flexibilidad espacio-temporal: El diseño mixto permite que agentes (estudiantes, tutores, docentes) articulen sesiones presenciales para prácticas y evaluación, con trabajo a distancia para contenidos teóricos y asesorías. Esto facilita el acceso y la posibilidad de estancias en centros educativos diversos.\\n- Recursos didácticos y tecnológicos: Materiales accesibles (texto enriquecido, transcripciones, subtítulos, lecturas en formatos alternativos) y simuladores para entrenamiento de situaciones complejas. Herramientas para registro y análisis de observación (plantillas digitales, apps para registro de conductas) soportarán la formación práctica.\\n- Vinculación con asignaturas: Las plataformas alojarán los recursos de cada asignatura, enlaces a prácticas supervisadas, y espacios de interacción interprofesional. Los procesos de evaluación combinarán evidencias en línea y en campo, asegurando validez y confiabilidad.\\n- Soporte institucional: Tutorías académicas y tecnopedagógicas, acompañamiento para prácticas y mesas de análisis de casos. Se garantizará la interoperabilidad entre las actividades presenciales y virtuales para mantener coherencia pedagógica y cumplimiento de competencias.\",\n",
|
||||||
|
" \"administracion_y_operatividad_del_plan_de_estudios\":\n",
|
||||||
|
" \"Estructura administrativa y operativa:\\n- Coordinación académica: Existirá una Coordinación de la Licenciatura responsable del seguimiento académico, la vinculación con centros de práctica y la supervisión de procesos de aseguramiento de la calidad.\\n- Organización de la oferta: Diseño rígido en 9 semestres, con secuencia establecida de asignaturas obligatorias y optativas estructuradas por niveles formativos.\\n- Carga por ciclo: La carga promedio por semestre se organizará en torno a 40 horas semanales efectivas, que incluyen docencia teórica, actividades prácticas y tiempo de estudio guiado.\\n- Prácticas y estancias: Prácticas profesionales progresivas desde el segundo semestre, con incremento de horas prácticas hasta culminar en estancias intensivas en contextos escolares en el último año. Se definirá un manual de prácticas con perfiles de supervisores y criterios de evaluación.\\n- Plantilla docente: Perfil docente con grado mínimo de maestría en áreas afines, experiencia en inclusión y prácticas de sombra, y formación en investigación aplicada. Se promoverá la actualización continua y la supervisión por expertos.\\n- Gestión curricular: Cronograma anual, sesiones de coordinación docente por asignatura, sistema de registro de evidencias curriculares y control de avance estudiantil.\\n- Siembra de asignaturas por semestre (ejemplo operativo): Semestre 1-3: fundamentos y diagnóstico; Semestre 4-6: intervención y metodologías; Semestre 7-9: prácticas intensivas, proyectos integradores y seminario de titulación.\\n- Tutoría académica: Cada estudiante contará con un tutor académico y un tutor de prácticas, responsables de seguimiento del avance y apoyo en la articulación teoría-práctica.\",\n",
|
||||||
|
" \"propuesta_de_evaluacion_periodica_del_plan_de_estudios\":\n",
|
||||||
|
" \"Metodología de evaluación del plan de estudios:\\n- Objetivo: Mantener actualización curricular y pertinencia disciplinar mediante evaluación anual y revisiones integrales cada tres años.\\n- Instrumentos: Encuestas de egresados y empleadores, análisis de desempeño en prácticas, revisión de trayectorias académicas, indicadores de inserción laboral y evaluación por pares académicos.\\n- Periodicidad: Informes anuales de seguimiento; revisión curricular bianual focalizada en asignaturas clave; evaluación integral cada 3 años con participación de comité externo.\\n- Procedimiento: Recolección de datos cuantitativos y cualitativos, análisis por la coordinación, diseño de plan de mejora con responsables y plazos, seguimiento de indicadores y reporte público de avances.\\n- Indicadores clave: Tasa de titulación, desempeño en prácticas (evaluación de supervisores), satisfacción de empleadores, cumplimiento de cargas académicas, actualización de contenidos y vinculación con evidencia científica.\\n- Participación: Incluye estudiantes, docentes, empleadores, instituciones receptoras de prácticas y expertos externos para asegurar validez y pertinencia.\",\n",
|
||||||
|
" \"nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios\":\n",
|
||||||
|
" \"Dr. Francisco Javier Ortega, Rector\",\n",
|
||||||
|
" },\n",
|
||||||
|
" convertTo: \"docx\",\n",
|
||||||
|
" reportName: \"anexo1.pdf\",\n",
|
||||||
|
" },\n",
|
||||||
|
" { download: true },\n",
|
||||||
|
");\n",
|
||||||
|
"\n",
|
||||||
|
"await Deno.writeFile(\"../anexo1.docx\", result.buffer);\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "29cdb42f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"const result = await carbone.render(\n",
|
||||||
|
" templateID,\n",
|
||||||
|
" {\n",
|
||||||
|
" data: {\n",
|
||||||
|
" \"vigencia\": \"2026\",\n",
|
||||||
|
" \"area_de_estudio\":\n",
|
||||||
|
" \"Educación. Campo amplio: Formación docente y educación especial (CMPE 2016, INEGI)\",\n",
|
||||||
|
" \"perfil_de_egreso\":\n",
|
||||||
|
" \"Conocimientos:\\n- Conocer los fundamentos teóricos y normativos de la educación especial y la inclusión educativa, incluidas las leyes y políticas nacionales aplicables.\\n- Conocer modelos de atención educativa (modelo ecológico, enfoque por competencias, Diseño Universal para el Aprendizaje, respuesta a la intervención) y sus implicaciones para la práctica de sombra.\\n- Conocer características y necesidades educativas de estudiantes con discapacidad, trastornos del desarrollo y necesidades educativas especiales específicas.\\n\\nHabilidades:\\n- Diseñar y aplicar estrategias individualizadas de acompañamiento (sombra) para facilitar la participación y el aprendizaje en contextos educativos regulares.\\n- Realizar observaciones sistemáticas, registros conductuales y adaptaciones curriculares básicas en colaboración con el personal docente y familias.\\n- Colaborar en equipos interdisciplinarios y comunicarse efectivamente con docentes, familias y especialistas para articular apoyos.\\n- Aplicar procedimientos de evaluación formativa y ajustar estrategias de intervención basadas en evidencia.\\n\\nActitudes y valores:\\n- Actuar con ética profesional, confidencialidad y respeto hacia la dignidad, autonomía y derechos de las personas con discapacidad.\\n- Mantener una actitud reflexiva, crítica y de mejora continua sustentada en la evidencia y en el aprendizaje colaborativo.\\n- Mostrar compromiso por la inclusión, la equidad y la promoción de entornos accesibles y participativos.\",\n",
|
||||||
|
" \"diseno_curricular\": \"Rígido\",\n",
|
||||||
|
" \"perfil_de_ingreso\":\n",
|
||||||
|
" \"Conocimientos previos:\\n- Certificado de Bachillerato o equivalente con dominio básico de lectura, redacción y razonamiento lógico-matemático.\\n- Conocimientos introductorios en psicología, pedagogía o atención a la diversidad (deseable, no obligatorio).\\n\\nHabilidades y aptitudes:\\n- Habilidad comunicativa efectiva y disposición para el trabajo en equipo.\\n- Capacidad de observación, registro y análisis de comportamientos en contextos educativos.\\n- Actitud empática, paciente y respetuosa frente a la diversidad funcional y cultural.\\n\\nCondiciones personales:\\n- Disponibilidad para prácticas en centros educativos y visitas domiciliarias.\\n- Compromiso ético y disposición para formación continua y trabajo con poblaciones vulnerables.\",\n",
|
||||||
|
" \"curso_propedeutico\":\n",
|
||||||
|
" \"Se ofrecerá un curso propedéutico opcional dirigido a aspirantes que no cuenten con formación previa en pedagogía o atención a la diversidad. Características:\\n- Población: Aspirantes sin experiencia formal en educación o con antecedentes en áreas no afines.\\n- Duración: 8 semanas (40 horas totales), modalidad mixta (sincrónico y asincrónico).\\n- Contenidos: Fundamentos de psicología del desarrollo, nociones básicas de pedagogía inclusiva, comunicación efectiva y manejo básico de registros.\\n- Relación con el plan de estudios: Prepara al aspirante para cursar asignaturas iniciales de didáctica y atención a la diversidad; no substituye requisitos de admisión, pero fortalece rendimiento académico inicial.\\n- Criterios de aplicación: Se recomienda para el 30% de la cohorte cuya evaluación de diagnóstico inicial identifique brechas relevantes en competencias lectoras, comunicativas o conocimientos básicos en educación.\",\n",
|
||||||
|
" \"modalidad_educativa\": \"Mixta\",\n",
|
||||||
|
" \"antecedente_academico\":\n",
|
||||||
|
" \"Certificado de Bachillerato o equivalente. Preferentemente procedencia de áreas afines como Ciencias de la Educación, Psicología, Trabajo Social, Pedagogía o áreas de ciencias sociales relacionadas con atención a la infancia y discapacidad.\",\n",
|
||||||
|
" \"carga_horaria_a_la_semana\": 40,\n",
|
||||||
|
" \"programa_de_investigacion\": null,\n",
|
||||||
|
" \"clave_del_plan_de_estudios\": \"LLS-LESE-2026\",\n",
|
||||||
|
" \"duracion_del_ciclo_escolar\": \"16 semanas\",\n",
|
||||||
|
" \"fines_de_aprendizaje_o_formacion\":\n",
|
||||||
|
" \"El programa tiene por finalidad formar profesionales de la educación especializados en el acompañamiento tipo 'sombra', capaces de intervenir en contextos educativos regulares y de atención especializada mediante prácticas fundamentadas en la evidencia, la colaboración interprofesional y el respeto a los derechos de las personas con discapacidad. Los fines de aprendizaje se articulan en competencias que guían la formación:\\n- Analizar los marcos normativos, éticos y teóricos que sustentan la educación especial y la inclusión, con el fin de garantizar intervenciones respetuosas de los derechos y la dignidad de las personas con discapacidad.\\n- Identificar las características desarrollo-cognitivas, socioemocionales y comunicativas de estudiantes con diferentes tipos de discapacidad o necesidades educativas especiales, para adecuar estrategias de apoyo.\\n- Diseñar planes de acompañamiento individualizados basados en la evaluación formativa, que integren adaptaciones curriculares, apoyos conductuales y mediaciones pedagógicas pertinentes al contexto escolar.\\n- Implementar técnicas de observación, registro y evaluación del comportamiento y del progreso académico, utilizando instrumentos estandarizados y protocolos de registro confiables.\\n- Colaborar y coordinar con docentes titulares, equipos interdisciplinares y familias para articular apoyos, comunicando hallazgos y proponiendo ajustes en el entorno y la práctica pedagógica.\\n- Implementar estrategias de mediación y apoyo que promuevan la participación activa, la autonomía funcional y la inclusión social del alumno dentro del aula regular.\\n- Evaluar la eficacia de las intervenciones de sombra mediante indicadores cualitativos y cuantitativos, ajustando las acciones en ciclos de mejora continua.\\n- Promover entornos accesibles y diseñar ajustes razonables y recursos didácticos adaptados que favorezcan el aprendizaje y la interacción social.\\n- Fomentar actitudes profesionales basadas en la ética, la confidencialidad, el respeto a la diversidad cultural y la toma de decisiones centrada en la persona.\\n- Difundir prácticas basadas en evidencia y favorecer la investigación aplicada en contextos escolares para la mejora de modelos de acompañamiento y políticas inclusivas.\",\n",
|
||||||
|
" \"nivel_y_nombre_del_plan_de_estudios\":\n",
|
||||||
|
" \"Licenciatura en Educación Especial (orientación: Sombra)\",\n",
|
||||||
|
" \"nombre_autorizado_de_la_institucion\":\n",
|
||||||
|
" \"Universidad La Salle México\",\n",
|
||||||
|
" \"total_de_ciclos_del_plan_de_estudios\": \"9\",\n",
|
||||||
|
" \"sustento_teorico_del_modelo_curricular\":\n",
|
||||||
|
" \"Lista de pasos conceptuales previos:\\n- Revisar requisitoss normativos del Acuerdo 17/11/17 y la normativa educativa nacional aplicable.\\n- Estructurar campos obligatorios del Anexo 1 y verificar coherencia entre ingreso, perfil y fines de aprendizaje.\\n- Diseñar el marco curricular alineado con teorías de inclusión, UDL y prácticas basadas en evidencia.\\n- Determinar la operatividad administrativa y los mecanismos de evaluación periódica.\\n- Validar la carga horaria, duración de ciclos y practicum con estándares nacionales y de buenas prácticas.\\n\\nMarco teórico y justificación del modelo curricular:\\n El modelo curricular de la Licenciatura en Educación Especial con orientación en acompañamiento tipo sombra parte de la premisa de que la inclusión educativa es un derecho humano y una responsabilidad institucional que requiere profesionales formados de manera específica para acompañar procesos de aprendizaje y participación en contextos escolares regulares. Se sustenta en los siguientes referentes teóricos y conceptuales:\\n - Diseño Universal para el Aprendizaje (UDL): plantea principios para anticipar barreras y diseñar múltiples vías de acceso al currículo, favoreciendo la planificación de adaptaciones y apoyos que el profesional en sombra debe conocer y aplicar.\\n - Enfoque ecológico y bio-psicosocial: reconoce que las necesidades de los estudiantes emergen de la interacción entre individuo, familia, escuela y comunidad; por tanto, la intervención del profesional sombra se concibe como mediación contextual, no como sustitución del docente.\\n - Teorías socioculturales del aprendizaje (Vygotsky): sustentan la importancia del andamiaje y la zona de desarrollo próximo, herramientas clave para la intervención individualizada y para promover la autonomía del estudiante mediante mediación intencional.\\n - Modelos colaborativos y de co-intervención: el plan se apoya en evidencia sobre la efectividad de prácticas colaborativas entre docente titular, especialista y familia para lograr inclusión sostenible; el profesional sombra es formado para integrarse de forma articulada en estos equipos.\\n - Prácticas basadas en evidencia y evaluación formativa: el currículo incorpora métodos de observación sistemática, medición del progreso y diseño de intervenciones ajustadas a resultados, promoviendo la investigación aplicada y la cultura de evaluación.\\n - Perspectiva de derechos y normativa nacional: el modelo articula la formación con el marco legal mexicano en materia educativa y de discapacidad, así como con principios de ética profesional y confidencialidad.\\n\\n La estructura curricular combina contenidos conceptuales, metodológicos y prácticos; integra prácticas supervisadas desde el primer año, seminarios de reflexión profesional, y proyectos integradores que vinculan investigación aplicada con mejora institucional. La formación busca que el egresado maneje herramientas para la evaluación, diseño e implementación de apoyos que fomenten la inclusión, la participación y el desarrollo integral del estudiante.\",\n",
|
||||||
|
" \"justificacion_de_la_propuesta_curricular\":\n",
|
||||||
|
" \"Modalidad mixta: explicación de implementación tecnológica, autogestión y flexibilidad espacio-temporal.\\n- Plataforma tecnológica: El programa empleará un LMS institucional (por ejemplo, Moodle institucional o plataforma propietaria) que soporta contenidos asincrónicos (módulos temáticos, lecturas, foros), actividades evaluables y repositorio de recursos adaptativos. Además se dispondrá de herramientas para videoconferencia (Zoom/Teams) para sesiones sincrónicas, tutorías y supervisión de práctica.\\n- Autogestión del aprendizaje: Las actividades asincrónicas incluirán guías de estudio, rúbricas claras y recursos multimedia. Se promoverá la metacognición mediante diarios de aprendizaje y portafolios electrónicos que permitan evidenciar competencias profesionales.\\n- Flexibilidad espacio-temporal: El diseño mixto permite que agentes (estudiantes, tutores, docentes) articulen sesiones presenciales para prácticas y evaluación, con trabajo a distancia para contenidos teóricos y asesorías. Esto facilita el acceso y la posibilidad de estancias en centros educativos diversos.\\n- Recursos didácticos y tecnológicos: Materiales accesibles (texto enriquecido, transcripciones, subtítulos, lecturas en formatos alternativos) y simuladores para entrenamiento de situaciones complejas. Herramientas para registro y análisis de observación (plantillas digitales, apps para registro de conductas) soportarán la formación práctica.\\n- Vinculación con asignaturas: Las plataformas alojarán los recursos de cada asignatura, enlaces a prácticas supervisadas, y espacios de interacción interprofesional. Los procesos de evaluación combinarán evidencias en línea y en campo, asegurando validez y confiabilidad.\\n- Soporte institucional: Tutorías académicas y tecnopedagógicas, acompañamiento para prácticas y mesas de análisis de casos. Se garantizará la interoperabilidad entre las actividades presenciales y virtuales para mantener coherencia pedagógica y cumplimiento de competencias.\",\n",
|
||||||
|
" \"administracion_y_operatividad_del_plan_de_estudios\":\n",
|
||||||
|
" \"Estructura administrativa y operativa:\\n- Coordinación académica: Existirá una Coordinación de la Licenciatura responsable del seguimiento académico, la vinculación con centros de práctica y la supervisión de procesos de aseguramiento de la calidad.\\n- Organización de la oferta: Diseño rígido en 9 semestres, con secuencia establecida de asignaturas obligatorias y optativas estructuradas por niveles formativos.\\n- Carga por ciclo: La carga promedio por semestre se organizará en torno a 40 horas semanales efectivas, que incluyen docencia teórica, actividades prácticas y tiempo de estudio guiado.\\n- Prácticas y estancias: Prácticas profesionales progresivas desde el segundo semestre, con incremento de horas prácticas hasta culminar en estancias intensivas en contextos escolares en el último año. Se definirá un manual de prácticas con perfiles de supervisores y criterios de evaluación.\\n- Plantilla docente: Perfil docente con grado mínimo de maestría en áreas afines, experiencia en inclusión y prácticas de sombra, y formación en investigación aplicada. Se promoverá la actualización continua y la supervisión por expertos.\\n- Gestión curricular: Cronograma anual, sesiones de coordinación docente por asignatura, sistema de registro de evidencias curriculares y control de avance estudiantil.\\n- Siembra de asignaturas por semestre (ejemplo operativo): Semestre 1-3: fundamentos y diagnóstico; Semestre 4-6: intervención y metodologías; Semestre 7-9: prácticas intensivas, proyectos integradores y seminario de titulación.\\n- Tutoría académica: Cada estudiante contará con un tutor académico y un tutor de prácticas, responsables de seguimiento del avance y apoyo en la articulación teoría-práctica.\",\n",
|
||||||
|
" \"propuesta_de_evaluacion_periodica_del_plan_de_estudios\":\n",
|
||||||
|
" \"Metodología de evaluación del plan de estudios:\\n- Objetivo: Mantener actualización curricular y pertinencia disciplinar mediante evaluación anual y revisiones integrales cada tres años.\\n- Instrumentos: Encuestas de egresados y empleadores, análisis de desempeño en prácticas, revisión de trayectorias académicas, indicadores de inserción laboral y evaluación por pares académicos.\\n- Periodicidad: Informes anuales de seguimiento; revisión curricular bianual focalizada en asignaturas clave; evaluación integral cada 3 años con participación de comité externo.\\n- Procedimiento: Recolección de datos cuantitativos y cualitativos, análisis por la coordinación, diseño de plan de mejora con responsables y plazos, seguimiento de indicadores y reporte público de avances.\\n- Indicadores clave: Tasa de titulación, desempeño en prácticas (evaluación de supervisores), satisfacción de empleadores, cumplimiento de cargas académicas, actualización de contenidos y vinculación con evidencia científica.\\n- Participación: Incluye estudiantes, docentes, empleadores, instituciones receptoras de prácticas y expertos externos para asegurar validez y pertinencia.\",\n",
|
||||||
|
" \"nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios\":\n",
|
||||||
|
" \"Dr. Francisco Javier Ortega, Rector\",\n",
|
||||||
|
" },\n",
|
||||||
|
" convertTo: \"pdf\",\n",
|
||||||
|
" reportName: \"anexo1.pdf\",\n",
|
||||||
|
" },\n",
|
||||||
|
" { download: true },\n",
|
||||||
|
");\n",
|
||||||
|
"\n",
|
||||||
|
"await Deno.writeFile(\"../dist/anexo1.pdf\", result.buffer);\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "18fec251",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"{\n",
|
||||||
|
" success: true,\n",
|
||||||
|
" data: [\n",
|
||||||
|
" {\n",
|
||||||
|
" versionId: \"1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6\",\n",
|
||||||
|
" id: \"1373945625988937236\",\n",
|
||||||
|
" deployedAt: 1773246290,\n",
|
||||||
|
" createdAt: 1773246316,\n",
|
||||||
|
" expireAt: 0,\n",
|
||||||
|
" size: 29008,\n",
|
||||||
|
" type: \"docx\",\n",
|
||||||
|
" name: \"Anexo 1\",\n",
|
||||||
|
" category: \"Planes de estudio\",\n",
|
||||||
|
" comment: \"Plantillaaa de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n",
|
||||||
|
" tags: [],\n",
|
||||||
|
" origin: 1\n",
|
||||||
|
" }\n",
|
||||||
|
" ],\n",
|
||||||
|
" hasMore: false\n",
|
||||||
|
"}\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"const versions = await carbone.listTemplates({\n",
|
||||||
|
" id: \"1373945625988937236\",\n",
|
||||||
|
" includeVersions: true,\n",
|
||||||
|
"});\n",
|
||||||
|
"\n",
|
||||||
|
"console.log(versions);\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "fc4b1068",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"{\n",
|
||||||
|
" success: true,\n",
|
||||||
|
" data: { renderId: \"xaVIgwAb09fKX_0iLOFYawYW5leG8xLnBkZg.pdf\" }\n",
|
||||||
|
"}\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"const result = await carbone.render(\n",
|
||||||
|
" templateID,\n",
|
||||||
|
" {\n",
|
||||||
|
" data: {\n",
|
||||||
|
" \"vigencia\": \"2026\",\n",
|
||||||
|
" \"area_de_estudio\":\n",
|
||||||
|
" \"Educación. Campo amplio: Formación docente y educación especial (CMPE 2016, INEGI)\",\n",
|
||||||
|
" \"perfil_de_egreso\":\n",
|
||||||
|
" \"Conocimientos:\\n- Conocer los fundamentos teóricos y normativos de la educación especial y la inclusión educativa, incluidas las leyes y políticas nacionales aplicables.\\n- Conocer modelos de atención educativa (modelo ecológico, enfoque por competencias, Diseño Universal para el Aprendizaje, respuesta a la intervención) y sus implicaciones para la práctica de sombra.\\n- Conocer características y necesidades educativas de estudiantes con discapacidad, trastornos del desarrollo y necesidades educativas especiales específicas.\\n\\nHabilidades:\\n- Diseñar y aplicar estrategias individualizadas de acompañamiento (sombra) para facilitar la participación y el aprendizaje en contextos educativos regulares.\\n- Realizar observaciones sistemáticas, registros conductuales y adaptaciones curriculares básicas en colaboración con el personal docente y familias.\\n- Colaborar en equipos interdisciplinarios y comunicarse efectivamente con docentes, familias y especialistas para articular apoyos.\\n- Aplicar procedimientos de evaluación formativa y ajustar estrategias de intervención basadas en evidencia.\\n\\nActitudes y valores:\\n- Actuar con ética profesional, confidencialidad y respeto hacia la dignidad, autonomía y derechos de las personas con discapacidad.\\n- Mantener una actitud reflexiva, crítica y de mejora continua sustentada en la evidencia y en el aprendizaje colaborativo.\\n- Mostrar compromiso por la inclusión, la equidad y la promoción de entornos accesibles y participativos.\",\n",
|
||||||
|
" \"diseno_curricular\": \"Rígido\",\n",
|
||||||
|
" \"perfil_de_ingreso\":\n",
|
||||||
|
" \"Conocimientos previos:\\n- Certificado de Bachillerato o equivalente con dominio básico de lectura, redacción y razonamiento lógico-matemático.\\n- Conocimientos introductorios en psicología, pedagogía o atención a la diversidad (deseable, no obligatorio).\\n\\nHabilidades y aptitudes:\\n- Habilidad comunicativa efectiva y disposición para el trabajo en equipo.\\n- Capacidad de observación, registro y análisis de comportamientos en contextos educativos.\\n- Actitud empática, paciente y respetuosa frente a la diversidad funcional y cultural.\\n\\nCondiciones personales:\\n- Disponibilidad para prácticas en centros educativos y visitas domiciliarias.\\n- Compromiso ético y disposición para formación continua y trabajo con poblaciones vulnerables.\",\n",
|
||||||
|
" \"curso_propedeutico\":\n",
|
||||||
|
" \"Se ofrecerá un curso propedéutico opcional dirigido a aspirantes que no cuenten con formación previa en pedagogía o atención a la diversidad. Características:\\n- Población: Aspirantes sin experiencia formal en educación o con antecedentes en áreas no afines.\\n- Duración: 8 semanas (40 horas totales), modalidad mixta (sincrónico y asincrónico).\\n- Contenidos: Fundamentos de psicología del desarrollo, nociones básicas de pedagogía inclusiva, comunicación efectiva y manejo básico de registros.\\n- Relación con el plan de estudios: Prepara al aspirante para cursar asignaturas iniciales de didáctica y atención a la diversidad; no substituye requisitos de admisión, pero fortalece rendimiento académico inicial.\\n- Criterios de aplicación: Se recomienda para el 30% de la cohorte cuya evaluación de diagnóstico inicial identifique brechas relevantes en competencias lectoras, comunicativas o conocimientos básicos en educación.\",\n",
|
||||||
|
" \"modalidad_educativa\": \"Mixta\",\n",
|
||||||
|
" \"antecedente_academico\":\n",
|
||||||
|
" \"Certificado de Bachillerato o equivalente. Preferentemente procedencia de áreas afines como Ciencias de la Educación, Psicología, Trabajo Social, Pedagogía o áreas de ciencias sociales relacionadas con atención a la infancia y discapacidad.\",\n",
|
||||||
|
" \"carga_horaria_a_la_semana\": 40,\n",
|
||||||
|
" \"programa_de_investigacion\": null,\n",
|
||||||
|
" \"clave_del_plan_de_estudios\": \"LLS-LESE-2026\",\n",
|
||||||
|
" \"duracion_del_ciclo_escolar\": \"16 semanas\",\n",
|
||||||
|
" \"fines_de_aprendizaje_o_formacion\":\n",
|
||||||
|
" \"El programa tiene por finalidad formar profesionales de la educación especializados en el acompañamiento tipo 'sombra', capaces de intervenir en contextos educativos regulares y de atención especializada mediante prácticas fundamentadas en la evidencia, la colaboración interprofesional y el respeto a los derechos de las personas con discapacidad. Los fines de aprendizaje se articulan en competencias que guían la formación:\\n- Analizar los marcos normativos, éticos y teóricos que sustentan la educación especial y la inclusión, con el fin de garantizar intervenciones respetuosas de los derechos y la dignidad de las personas con discapacidad.\\n- Identificar las características desarrollo-cognitivas, socioemocionales y comunicativas de estudiantes con diferentes tipos de discapacidad o necesidades educativas especiales, para adecuar estrategias de apoyo.\\n- Diseñar planes de acompañamiento individualizados basados en la evaluación formativa, que integren adaptaciones curriculares, apoyos conductuales y mediaciones pedagógicas pertinentes al contexto escolar.\\n- Implementar técnicas de observación, registro y evaluación del comportamiento y del progreso académico, utilizando instrumentos estandarizados y protocolos de registro confiables.\\n- Colaborar y coordinar con docentes titulares, equipos interdisciplinares y familias para articular apoyos, comunicando hallazgos y proponiendo ajustes en el entorno y la práctica pedagógica.\\n- Implementar estrategias de mediación y apoyo que promuevan la participación activa, la autonomía funcional y la inclusión social del alumno dentro del aula regular.\\n- Evaluar la eficacia de las intervenciones de sombra mediante indicadores cualitativos y cuantitativos, ajustando las acciones en ciclos de mejora continua.\\n- Promover entornos accesibles y diseñar ajustes razonables y recursos didácticos adaptados que favorezcan el aprendizaje y la interacción social.\\n- Fomentar actitudes profesionales basadas en la ética, la confidencialidad, el respeto a la diversidad cultural y la toma de decisiones centrada en la persona.\\n- Difundir prácticas basadas en evidencia y favorecer la investigación aplicada en contextos escolares para la mejora de modelos de acompañamiento y políticas inclusivas.\",\n",
|
||||||
|
" \"nivel_y_nombre_del_plan_de_estudios\":\n",
|
||||||
|
" \"Licenciatura en Educación Especial (orientación: Sombra)\",\n",
|
||||||
|
" \"nombre_autorizado_de_la_institucion\":\n",
|
||||||
|
" \"Universidad La Salle México\",\n",
|
||||||
|
" \"total_de_ciclos_del_plan_de_estudios\": \"9\",\n",
|
||||||
|
" \"sustento_teorico_del_modelo_curricular\":\n",
|
||||||
|
" \"Lista de pasos conceptuales previos:\\n- Revisar requisitoss normativos del Acuerdo 17/11/17 y la normativa educativa nacional aplicable.\\n- Estructurar campos obligatorios del Anexo 1 y verificar coherencia entre ingreso, perfil y fines de aprendizaje.\\n- Diseñar el marco curricular alineado con teorías de inclusión, UDL y prácticas basadas en evidencia.\\n- Determinar la operatividad administrativa y los mecanismos de evaluación periódica.\\n- Validar la carga horaria, duración de ciclos y practicum con estándares nacionales y de buenas prácticas.\\n\\nMarco teórico y justificación del modelo curricular:\\n El modelo curricular de la Licenciatura en Educación Especial con orientación en acompañamiento tipo sombra parte de la premisa de que la inclusión educativa es un derecho humano y una responsabilidad institucional que requiere profesionales formados de manera específica para acompañar procesos de aprendizaje y participación en contextos escolares regulares. Se sustenta en los siguientes referentes teóricos y conceptuales:\\n - Diseño Universal para el Aprendizaje (UDL): plantea principios para anticipar barreras y diseñar múltiples vías de acceso al currículo, favoreciendo la planificación de adaptaciones y apoyos que el profesional en sombra debe conocer y aplicar.\\n - Enfoque ecológico y bio-psicosocial: reconoce que las necesidades de los estudiantes emergen de la interacción entre individuo, familia, escuela y comunidad; por tanto, la intervención del profesional sombra se concibe como mediación contextual, no como sustitución del docente.\\n - Teorías socioculturales del aprendizaje (Vygotsky): sustentan la importancia del andamiaje y la zona de desarrollo próximo, herramientas clave para la intervención individualizada y para promover la autonomía del estudiante mediante mediación intencional.\\n - Modelos colaborativos y de co-intervención: el plan se apoya en evidencia sobre la efectividad de prácticas colaborativas entre docente titular, especialista y familia para lograr inclusión sostenible; el profesional sombra es formado para integrarse de forma articulada en estos equipos.\\n - Prácticas basadas en evidencia y evaluación formativa: el currículo incorpora métodos de observación sistemática, medición del progreso y diseño de intervenciones ajustadas a resultados, promoviendo la investigación aplicada y la cultura de evaluación.\\n - Perspectiva de derechos y normativa nacional: el modelo articula la formación con el marco legal mexicano en materia educativa y de discapacidad, así como con principios de ética profesional y confidencialidad.\\n\\n La estructura curricular combina contenidos conceptuales, metodológicos y prácticos; integra prácticas supervisadas desde el primer año, seminarios de reflexión profesional, y proyectos integradores que vinculan investigación aplicada con mejora institucional. La formación busca que el egresado maneje herramientas para la evaluación, diseño e implementación de apoyos que fomenten la inclusión, la participación y el desarrollo integral del estudiante.\",\n",
|
||||||
|
" \"justificacion_de_la_propuesta_curricular\":\n",
|
||||||
|
" \"Modalidad mixta: explicación de implementación tecnológica, autogestión y flexibilidad espacio-temporal.\\n- Plataforma tecnológica: El programa empleará un LMS institucional (por ejemplo, Moodle institucional o plataforma propietaria) que soporta contenidos asincrónicos (módulos temáticos, lecturas, foros), actividades evaluables y repositorio de recursos adaptativos. Además se dispondrá de herramientas para videoconferencia (Zoom/Teams) para sesiones sincrónicas, tutorías y supervisión de práctica.\\n- Autogestión del aprendizaje: Las actividades asincrónicas incluirán guías de estudio, rúbricas claras y recursos multimedia. Se promoverá la metacognición mediante diarios de aprendizaje y portafolios electrónicos que permitan evidenciar competencias profesionales.\\n- Flexibilidad espacio-temporal: El diseño mixto permite que agentes (estudiantes, tutores, docentes) articulen sesiones presenciales para prácticas y evaluación, con trabajo a distancia para contenidos teóricos y asesorías. Esto facilita el acceso y la posibilidad de estancias en centros educativos diversos.\\n- Recursos didácticos y tecnológicos: Materiales accesibles (texto enriquecido, transcripciones, subtítulos, lecturas en formatos alternativos) y simuladores para entrenamiento de situaciones complejas. Herramientas para registro y análisis de observación (plantillas digitales, apps para registro de conductas) soportarán la formación práctica.\\n- Vinculación con asignaturas: Las plataformas alojarán los recursos de cada asignatura, enlaces a prácticas supervisadas, y espacios de interacción interprofesional. Los procesos de evaluación combinarán evidencias en línea y en campo, asegurando validez y confiabilidad.\\n- Soporte institucional: Tutorías académicas y tecnopedagógicas, acompañamiento para prácticas y mesas de análisis de casos. Se garantizará la interoperabilidad entre las actividades presenciales y virtuales para mantener coherencia pedagógica y cumplimiento de competencias.\",\n",
|
||||||
|
" \"administracion_y_operatividad_del_plan_de_estudios\":\n",
|
||||||
|
" \"Estructura administrativa y operativa:\\n- Coordinación académica: Existirá una Coordinación de la Licenciatura responsable del seguimiento académico, la vinculación con centros de práctica y la supervisión de procesos de aseguramiento de la calidad.\\n- Organización de la oferta: Diseño rígido en 9 semestres, con secuencia establecida de asignaturas obligatorias y optativas estructuradas por niveles formativos.\\n- Carga por ciclo: La carga promedio por semestre se organizará en torno a 40 horas semanales efectivas, que incluyen docencia teórica, actividades prácticas y tiempo de estudio guiado.\\n- Prácticas y estancias: Prácticas profesionales progresivas desde el segundo semestre, con incremento de horas prácticas hasta culminar en estancias intensivas en contextos escolares en el último año. Se definirá un manual de prácticas con perfiles de supervisores y criterios de evaluación.\\n- Plantilla docente: Perfil docente con grado mínimo de maestría en áreas afines, experiencia en inclusión y prácticas de sombra, y formación en investigación aplicada. Se promoverá la actualización continua y la supervisión por expertos.\\n- Gestión curricular: Cronograma anual, sesiones de coordinación docente por asignatura, sistema de registro de evidencias curriculares y control de avance estudiantil.\\n- Siembra de asignaturas por semestre (ejemplo operativo): Semestre 1-3: fundamentos y diagnóstico; Semestre 4-6: intervención y metodologías; Semestre 7-9: prácticas intensivas, proyectos integradores y seminario de titulación.\\n- Tutoría académica: Cada estudiante contará con un tutor académico y un tutor de prácticas, responsables de seguimiento del avance y apoyo en la articulación teoría-práctica.\",\n",
|
||||||
|
" \"propuesta_de_evaluacion_periodica_del_plan_de_estudios\":\n",
|
||||||
|
" \"Metodología de evaluación del plan de estudios:\\n- Objetivo: Mantener actualización curricular y pertinencia disciplinar mediante evaluación anual y revisiones integrales cada tres años.\\n- Instrumentos: Encuestas de egresados y empleadores, análisis de desempeño en prácticas, revisión de trayectorias académicas, indicadores de inserción laboral y evaluación por pares académicos.\\n- Periodicidad: Informes anuales de seguimiento; revisión curricular bianual focalizada en asignaturas clave; evaluación integral cada 3 años con participación de comité externo.\\n- Procedimiento: Recolección de datos cuantitativos y cualitativos, análisis por la coordinación, diseño de plan de mejora con responsables y plazos, seguimiento de indicadores y reporte público de avances.\\n- Indicadores clave: Tasa de titulación, desempeño en prácticas (evaluación de supervisores), satisfacción de empleadores, cumplimiento de cargas académicas, actualización de contenidos y vinculación con evidencia científica.\\n- Participación: Incluye estudiantes, docentes, empleadores, instituciones receptoras de prácticas y expertos externos para asegurar validez y pertinencia.\",\n",
|
||||||
|
" \"nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios\":\n",
|
||||||
|
" \"Dr. Francisco Javier Ortega, Rector\",\n",
|
||||||
|
" },\n",
|
||||||
|
" convertTo: \"pdf\",\n",
|
||||||
|
" reportName: \"anexo1.pdf\",\n",
|
||||||
|
" },\n",
|
||||||
|
" { download: false },\n",
|
||||||
|
");\n",
|
||||||
|
"\n",
|
||||||
|
"console.log(result);\n",
|
||||||
|
"\n",
|
||||||
|
"// const file = await carbone.downloadRender(render.data.renderId);\n",
|
||||||
|
"const file = await carbone.downloadRender(\n",
|
||||||
|
" \"X1Bx6Stre95Sm54jHxIwpQYW5leG8xLnBkZg.pdf\",\n",
|
||||||
|
");\n",
|
||||||
|
"await Deno.writeFile(\"dist/anexo1.pdf\", file.buffer);\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5fe4c4d1",
|
||||||
|
"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
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import ExcelJS from "exceljs";
|
||||||
|
import { applyMergePattern } from "./lib";
|
||||||
|
|
||||||
|
/* const inputFile = Bun.file("mapa.xlsx");
|
||||||
|
const inputBuffer = await inputFile.arrayBuffer();
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
await workbook.xlsx.load(inputBuffer); */
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
await workbook.xlsx.readFile("mapa.xlsx");
|
||||||
|
|
||||||
|
const sheetName = 'RÍGIDO-Anexo 2 (A)'
|
||||||
|
const sheet = workbook.getWorksheet(sheetName)
|
||||||
|
if (!sheet) {
|
||||||
|
throw new Error(`Workbook has no sheet named: ${sheetName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CICLOS = 9;
|
||||||
|
const matrizAsignaturas = {
|
||||||
|
startColumns: ["C", "I", "O", "U", "AA"],
|
||||||
|
firstStartRow: 8,
|
||||||
|
mergeWidthInColumns: 5,
|
||||||
|
mergeHeightInRows: 3,
|
||||||
|
mergeBlockCount: CICLOS,
|
||||||
|
rowStepBetweenBlocks: 14 - 8,
|
||||||
|
} as const;
|
||||||
|
const semestres = {
|
||||||
|
startColumns: ["A"],
|
||||||
|
firstStartRow: 7,
|
||||||
|
mergeWidthInColumns: 1,
|
||||||
|
mergeHeightInRows: 5,
|
||||||
|
mergeBlockCount: CICLOS,
|
||||||
|
rowStepBetweenBlocks: 13 - 7,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
applyMergePattern(sheet, matrizAsignaturas);
|
||||||
|
applyMergePattern(sheet, semestres);
|
||||||
|
|
||||||
|
const outputBuffer = await workbook.xlsx.writeBuffer();
|
||||||
|
await Bun.write("excel2.xlsx", outputBuffer);
|
||||||
|
|
||||||
|
const end = Bun.nanoseconds();
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { type Worksheet } from "exceljs";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Excel column label to a 1-based number.
|
||||||
|
* Example: A -> 1, Z -> 26, AA -> 27
|
||||||
|
*/
|
||||||
|
function columnToNumber(label: string): number {
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < label.length; i++) {
|
||||||
|
n = n * 26 + label.charCodeAt(i) - 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates merge ranges like:
|
||||||
|
* C8:G10, I8:M10, ...
|
||||||
|
* then C14:G16, I14:M16, ...
|
||||||
|
*/
|
||||||
|
type MergeRangePattern = {
|
||||||
|
startColumns: readonly string[];
|
||||||
|
firstStartRow: number;
|
||||||
|
mergeWidthInColumns: number;
|
||||||
|
mergeHeightInRows: number;
|
||||||
|
mergeBlockCount: number;
|
||||||
|
rowStepBetweenBlocks: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function applyMergePattern(sheet: Worksheet, config: MergeRangePattern) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
startColumns,
|
||||||
|
firstStartRow,
|
||||||
|
mergeWidthInColumns,
|
||||||
|
mergeHeightInRows,
|
||||||
|
mergeBlockCount,
|
||||||
|
rowStepBetweenBlocks
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
for (let block = 0; block < mergeBlockCount; block++) {
|
||||||
|
|
||||||
|
const rowStart = firstStartRow + block * rowStepBetweenBlocks;
|
||||||
|
const rowEnd = rowStart + mergeHeightInRows - 1;
|
||||||
|
|
||||||
|
for (const column of startColumns) {
|
||||||
|
|
||||||
|
const colStart = columnToNumber(column);
|
||||||
|
const colEnd = colStart + mergeWidthInColumns - 1;
|
||||||
|
|
||||||
|
sheet.mergeCells(rowStart, colStart, rowEnd, colEnd);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
+5
-1
@@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.90.1",
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
|
"@toon-format/toon": "^2.1.0",
|
||||||
|
"carbone-sdk": "^1.6.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,203 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"nombre_autorizado_de_la_institucion",
|
|
||||||
"nivel_y_nombre_del_plan_de_estudios",
|
|
||||||
"modalidad_educativa",
|
|
||||||
"vigencia",
|
|
||||||
"antecedente_academico",
|
|
||||||
"area_de_estudio",
|
|
||||||
"clave_del_plan_de_estudios",
|
|
||||||
"diseno_curricular",
|
|
||||||
"total_de_ciclos_del_plan_de_estudios",
|
|
||||||
"duracion_del_ciclo_escolar",
|
|
||||||
"carga_horaria_a_la_semana",
|
|
||||||
"fines_de_aprendizaje_o_formacion",
|
|
||||||
"perfil_de_egreso",
|
|
||||||
"programa_de_investigacion",
|
|
||||||
"curso_propedeutico",
|
|
||||||
"perfil_de_ingreso",
|
|
||||||
"administracion_y_operatividad_del_plan_de_estudios",
|
|
||||||
"sustento_teorico_del_modelo_curricular",
|
|
||||||
"justificacion_de_la_propuesta_curricular",
|
|
||||||
"propuesta_de_evaluacion_periodica_del_plan_de_estudios",
|
|
||||||
"nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"nombre_autorizado_de_la_institucion": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Nombre autorizado de la institución",
|
|
||||||
"examples": ["Universidad La Salle A.C."],
|
|
||||||
"description": "Proporcionar el nombre de la institución autorizado por SEP.",
|
|
||||||
"referencia_normativa": "Dato administrativo del Anexo 1."
|
|
||||||
},
|
|
||||||
"nivel_y_nombre_del_plan_de_estudios": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Nivel y nombre del plan de estudios",
|
|
||||||
"examples": [
|
|
||||||
"Licenciatura en Derecho",
|
|
||||||
"Maestría en Ciencias de la Computación"
|
|
||||||
],
|
|
||||||
"description": "Indicar el nivel educativo y denominación del plan de estudios.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito 'Nivel y denominación del plan de estudios' del Artículo 8, fracción I del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"modalidad_educativa": {
|
|
||||||
"enum": ["Escolar", "No escolarizada", "Mixta"],
|
|
||||||
"type": "string",
|
|
||||||
"title": "Modalidad educativa",
|
|
||||||
"examples": ["Escolar"],
|
|
||||||
"description": "Señalar la modalidad educativa en la que se impartirá el plan de estudios: escolar, no escolarizada o mixta.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito de 'Modalidad educativa' del Artículo 8, fracción II del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"vigencia": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Vigencia",
|
|
||||||
"examples": ["2024-2029"],
|
|
||||||
"description": "(Espacio para uso exclusivo de la autoridad educativa)",
|
|
||||||
"referencia_normativa": "Dato administrativo del Anexo 1."
|
|
||||||
},
|
|
||||||
"antecedente_academico": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Antecedente académico",
|
|
||||||
"examples": [
|
|
||||||
"Bachillerato concluido o equivalente.",
|
|
||||||
"Título de Licenciatura en áreas afines a la salud."
|
|
||||||
],
|
|
||||||
"description": "Señalar el nivel educativo requerido para ingresar al plan de estudios. Para posgrados, además del nivel educativo; indicar la(s) disciplina(s) o área(s) afín(es) al plan de estudios.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito 'Perfil de ingreso' (antecedente académico necesario) del Artículo 8, fracción V del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"area_de_estudio": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Área de estudio",
|
|
||||||
"examples": [
|
|
||||||
"Ingeniería, industria y construcción",
|
|
||||||
"Ciencias sociales y derecho"
|
|
||||||
],
|
|
||||||
"description": "Señalar el “campo amplio” al que pertenece el plan de estudios. Para ello, considerar la Clasificación Mexicana de Planes de Estudio por Campos de Formación Académica (CMPE 2016), publicado por INEGI.",
|
|
||||||
"referencia_normativa": "Deberá vincularse con el fin de aprendizaje y perfil de egreso del plan de estudios."
|
|
||||||
},
|
|
||||||
"clave_del_plan_de_estudios": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Clave / Año del plan",
|
|
||||||
"examples": ["2024"],
|
|
||||||
"description": "Señalar el año en el que solicita el trámite.",
|
|
||||||
"referencia_normativa": "Dato administrativo del Anexo 1."
|
|
||||||
},
|
|
||||||
"diseno_curricular": {
|
|
||||||
"enum": ["Rígido", "Flexible"],
|
|
||||||
"type": "string",
|
|
||||||
"title": "Diseño curricular",
|
|
||||||
"examples": ["Flexible"],
|
|
||||||
"description": "Indicar si es “Rígido” o “Flexible”.",
|
|
||||||
"referencia_normativa": "Define la estructura para el requisito 'Mapa curricular' del Artículo 8, fracción VII del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"total_de_ciclos_del_plan_de_estudios": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Número de ciclos escolares que integrarán el plan de estudio",
|
|
||||||
"examples": ["9 cuatrimestres", "8 semestres"],
|
|
||||||
"description": "En diseño flexible especificar mínimo y máximo de ciclos. En diseño rígido especificar el total de ciclos.",
|
|
||||||
"referencia_normativa": "Corresponde a 'señalando el número de ciclos en que se impartirá' del Artículo 8, fracción III del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"duracion_del_ciclo_escolar": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Duración mínima en semanas de cada ciclo escolar",
|
|
||||||
"examples": ["16 semanas"],
|
|
||||||
"description": "Señalar el número mínimo de semanas efectivas de clase que integran el tipo de ciclo en el que se imparte el plan de estudios.",
|
|
||||||
"referencia_normativa": "Corresponde exactamente al requisito 'Duración mínima en semanas' del Artículo 8, fracción III del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"carga_horaria_a_la_semana": {
|
|
||||||
"type": "number",
|
|
||||||
"title": "Carga horaria a la semana",
|
|
||||||
"examples": [25],
|
|
||||||
"description": "Indicar la máxima carga horaria impartida a la semana; no mayor a 50 horas bajo la conducción de un académico.",
|
|
||||||
"referencia_normativa": "Corresponde a la restricción 'sin exceder una carga máxima de 50 horas efectivas por semana' del Artículo 8, fracción III del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"fines_de_aprendizaje_o_formacion": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Fin de aprendizaje o formación",
|
|
||||||
"examples": [
|
|
||||||
"El estudiante analizará los principios fundamentales del derecho penal..."
|
|
||||||
],
|
|
||||||
"description": "Describir los aprendizajes que lograrán los estudiantes al concluir el plan de estudios. Deberá corresponder con el nivel educativo y la denominación del plan de estudios.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito 'Descripción de los fines del aprendizaje o formación' del Artículo 8, fracción IV del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"perfil_de_egreso": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Perfil de egreso",
|
|
||||||
"examples": ["El egresado será capaz de diseñar estrategias legales..."],
|
|
||||||
"description": "Describir los atributos que habrán desarrollado los estudiantes al concluir el plan de estudios, acordes al nivel educativo y denominación propuesta. De preferencia, dividirlo en conocimientos, habilidades y actitudes.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito 'Perfil de egreso' del Artículo 8, fracción VI del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"programa_de_investigacion": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"title": "Programa de investigación",
|
|
||||||
"examples": [
|
|
||||||
"Línea 1: Innovación Educativa. Objetivo: Analizar el impacto de..."
|
|
||||||
],
|
|
||||||
"description": "(Obligatorio para el nivel de Doctorado y para aquéllas Maestrías orientadas a la investigación). Presentar el programa de investigación que describa los objetivos del programa y líneas de investigación con sus respectivos objetivos. Asimismo, detallar la vinculación con las asignaturas que conforman el plan de estudios, la metodología y presentación de las investigaciones, así como el perfil de los docentes que guiarán su desarrollo.",
|
|
||||||
"referencia_normativa": "Requisito obligatorio solo para Doctorados o Maestrías con orientación a investigación según el Artículo 10, fracción III incisos b) y c) del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"curso_propedeutico": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"title": "Curso propedéutico",
|
|
||||||
"examples": [
|
|
||||||
"Curso de nivelación de 40 horas obligatorio para aspirantes de áreas ajenas."
|
|
||||||
],
|
|
||||||
"description": "Señalar las situaciones en las que aplicará el curso propedéutico, la población a la que estará dirigido, su duración, la relación con el plan de estudios, así como la información que considere relevante.",
|
|
||||||
"referencia_normativa": "Elemento opcional mencionado en las guías de llenado del Anexo 1."
|
|
||||||
},
|
|
||||||
"perfil_de_ingreso": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Perfil de ingreso",
|
|
||||||
"examples": [
|
|
||||||
"Conocimientos básicos de matemáticas, capacidad de análisis y redacción."
|
|
||||||
],
|
|
||||||
"description": "Indicar las condiciones académicas ideales (conocimientos, habilidades y aptitudes), para incorporarse al plan de estudios. Deberá vincularse con el nivel educativo y campo de estudio.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito 'Perfil de ingreso' (condiciones mínimas requeridas) del Artículo 8, fracción V del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"administracion_y_operatividad_del_plan_de_estudios": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"title": "Administración y operatividad del plan de estudios",
|
|
||||||
"examples": [
|
|
||||||
"El estudiante podrá inscribir un mínimo de 3 y un máximo de 6 asignaturas por ciclo..."
|
|
||||||
],
|
|
||||||
"description": "Describir la administración del plan de estudios, así como su operatividad; considerando el tipo de diseño curricular empleado. Si es flexible; incorporar el número mínimo y máximo de asignaturas que se cursarán por ciclo.",
|
|
||||||
"referencia_normativa": "Corresponde a la 'descripción detallada de la(s) forma(s) de administración y operatividad' permitida en el Artículo 8 (párrafo tercero) del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"sustento_teorico_del_modelo_curricular": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"title": "Sustento teórico del modelo curricular",
|
|
||||||
"examples": [
|
|
||||||
"Modelo por competencias profesionales fundamentado en el constructivismo..."
|
|
||||||
],
|
|
||||||
"description": "Presentar de forma concreta el marco conceptual en el que se justifica y fundamenta el diseño y organización curricular del plan de estudios.",
|
|
||||||
"referencia_normativa": "Corresponde al 'sustento teórico del modelo curricular' que el particular puede incluir según el Artículo 8 (párrafo tercero) del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"justificacion_de_la_propuesta_curricular": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"title": "Justificación de la propuesta curricular en la modalidad no escolarizada o mixta",
|
|
||||||
"examples": [
|
|
||||||
"Se utilizará una plataforma LMS asincrónica complementada con sesiones síncronas..."
|
|
||||||
],
|
|
||||||
"description": "(Exclusivo para modalidades no escolarizada y mixta). Describir cómo se impartirá el plan de estudios en la modalidad educativa elegida, los recursos tecnológicos y/o didácticos que emplearán para su desarrollo y su vinculación con las asignaturas y actividades de aprendizaje.",
|
|
||||||
"referencia_normativa": "Corresponde a lo solicitado en el Artículo 14 (justificación de modalidad) y Artículo 15 (especificaciones de plataforma/modelo) del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"propuesta_de_evaluacion_periodica_del_plan_de_estudios": {
|
|
||||||
"type": ["string", "null"],
|
|
||||||
"title": "Propuesta de evaluación periódica del plan de estudios",
|
|
||||||
"examples": [
|
|
||||||
"Revisión quinquenal mediante encuestas a empleadores, egresados y análisis de tendencias del mercado."
|
|
||||||
],
|
|
||||||
"description": "Describir de manera detallada la metodología que emplearán para la evaluación del plan de estudios, incluyendo los instrumentos y periodicidad en la que se llevará a cabo; con la finalidad de mantenerlo actualizado y acorde a las necesidades educativas y disciplinares.",
|
|
||||||
"referencia_normativa": "Corresponde al requisito 'Propuesta de evaluación periódica del Plan de estudio' del Artículo 8, fracción VIII del Acuerdo 17/11/17."
|
|
||||||
},
|
|
||||||
"nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Nombre y cargo de la persona facultada para autorizar el plan de estudios",
|
|
||||||
"examples": ["Dr. Juan Pérez Gómez, Rector de la Universidad XYZ"],
|
|
||||||
"description": "Indicar el nombre y cargo de la persona facultada para autorizar el plan de estudios.",
|
|
||||||
"referencia_normativa": "Dato administrativo del Anexo 1."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
create table "public"."tabla_prueba" (
|
|
||||||
"id" bigint generated by default as identity not null,
|
|
||||||
"created_at" timestamp with time zone not null default now(),
|
|
||||||
"columna_prueba" text
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
alter table "public"."tabla_prueba" enable row level security;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX tabla_prueba_pkey ON public.tabla_prueba USING btree (id);
|
|
||||||
|
|
||||||
alter table "public"."tabla_prueba" add constraint "tabla_prueba_pkey" PRIMARY KEY using index "tabla_prueba_pkey";
|
|
||||||
|
|
||||||
set check_function_bodies = off;
|
|
||||||
|
|
||||||
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$
|
|
||||||
;
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Ejecutar estos dos comandos para la generación de tipos de supabase:
|
||||||
|
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
dx supabase gen types typescript --local | Out-File -Encoding utf8 -FilePath "supabase\functions\_shared\database.types.ts"
|
||||||
@@ -1 +1 @@
|
|||||||
v2.75.0
|
v2.84.2
|
||||||
@@ -1 +1 @@
|
|||||||
v2.185.0
|
v2.187.0
|
||||||
@@ -1 +1 @@
|
|||||||
postgresql://postgres.exdkssurzmjnnhgtiama@aws-0-us-west-1.pooler.supabase.com:5432/postgres
|
postgresql://postgres.bxkskdxwppdlplrkidcz@aws-1-us-east-2.pooler.supabase.com:5432/postgres
|
||||||
@@ -1 +1 @@
|
|||||||
15.8.1.085
|
17.6.1.063
|
||||||
@@ -1 +1 @@
|
|||||||
exdkssurzmjnnhgtiama
|
bxkskdxwppdlplrkidcz
|
||||||
@@ -1 +1 @@
|
|||||||
v12.2.3
|
v14.1
|
||||||
@@ -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
|
||||||
@@ -37,3 +39,51 @@ entrypoint = "./functions/ai-generate-subject/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/ai-generate-subject/*.html" ]
|
# static_files = [ "./functions/ai-generate-subject/*.html" ]
|
||||||
|
|
||||||
|
[functions.generate-subject-suggestions]
|
||||||
|
enabled = true
|
||||||
|
verify_jwt = true
|
||||||
|
import_map = "./functions/generate-subject-suggestions/deno.json"
|
||||||
|
# Uncomment to specify a custom file path to the entrypoint.
|
||||||
|
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||||
|
entrypoint = "./functions/generate-subject-suggestions/index.ts"
|
||||||
|
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||||
|
# For example, if you want to serve static HTML pages in your function:
|
||||||
|
# static_files = [ "./functions/generate-subject-suggestions/*.html" ]
|
||||||
|
|
||||||
|
[auth.hook.custom_access_token]
|
||||||
|
enabled = true
|
||||||
|
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
||||||
|
|
||||||
|
[functions.openai-webhook-responses]
|
||||||
|
enabled = true
|
||||||
|
verify_jwt = false
|
||||||
|
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" ]
|
||||||
|
|
||||||
|
[functions.carbone-io-wrapper]
|
||||||
|
enabled = true
|
||||||
|
verify_jwt = true
|
||||||
|
import_map = "./functions/carbone-io-wrapper/deno.json"
|
||||||
|
# Uncomment to specify a custom file path to the entrypoint.
|
||||||
|
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||||
|
entrypoint = "./functions/carbone-io-wrapper/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/carbone-io-wrapper/*.html" ]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,205 +0,0 @@
|
|||||||
create extension if not exists "http" with schema "extensions";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" drop constraint "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 "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();
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
|||||||
|
export const definicionesDeEstructurasDeColumnas = {
|
||||||
|
contenido_tematico: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x-column": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"contenido_tematico",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"x-definicion": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"unidad": {
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"titulo": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"temas": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"nombre": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"horasEstimadas": {
|
||||||
|
"type": "integer",
|
||||||
|
"description":
|
||||||
|
"Horas del tema. La suma de todas las horasEstimadas debe ser igual a las horas académicas del prompt.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"nombre",
|
||||||
|
"horasEstimadas",
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"unidad",
|
||||||
|
"titulo",
|
||||||
|
"temas",
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"x-column",
|
||||||
|
"x-definicion",
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
},
|
||||||
|
criterios_de_evaluacion: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x-column": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"criterios_de_evaluacion",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"x-definicion": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"criterio": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"porcentaje": {
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"criterio",
|
||||||
|
"porcentaje",
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"x-column",
|
||||||
|
"x-definicion",
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
// supabase/functions/_shared/openai-service.ts
|
// supabase/functions/_shared/openai-service.ts
|
||||||
/// <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;
|
||||||
|
|
||||||
export type StructuredResponseSuccess<TOutput = unknown> = {
|
export type StructuredResponseSuccess<TOutput = unknown> = {
|
||||||
ok: true;
|
ok: true;
|
||||||
output?: TOutput; // parsed JSON when available
|
output?: TOutput; // parsed JSON when available
|
||||||
@@ -17,90 +14,64 @@ 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
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StructuredResponseFailure = {
|
export type StructuredResponseFailure = {
|
||||||
ok: false;
|
ok: false;
|
||||||
code:
|
code:
|
||||||
| "MissingEnv"
|
| "MissingEnv"
|
||||||
| "StorageUploadFailed"
|
|
||||||
| "OpenAIFileUploadFailed"
|
| "OpenAIFileUploadFailed"
|
||||||
| "OpenAIRequestFailed";
|
| "OpenAIRequestFailed";
|
||||||
message: string;
|
message: string;
|
||||||
cause?: unknown;
|
cause?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StructuredResponseResult<TOutput = unknown> =
|
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({
|
||||||
metadata,
|
metadata,
|
||||||
});
|
});
|
||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createStructuredResponse<TOutput = unknown>(
|
async createStructuredResponse<TOutput = unknown>(
|
||||||
options: StructuredResponseOptions,
|
options: StructuredResponseOptions,
|
||||||
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 };
|
||||||
|
|
||||||
// Attach file references to the request as an extra user message
|
// Attach file references to the request as an extra user message
|
||||||
if (openaiFileIds.length > 0) {
|
if (openaiFileIds.length > 0) {
|
||||||
const fileParts: OpenAITypes.OpenAI.Responses.ResponseInputFile[] =
|
const fileParts:
|
||||||
|
OpenAITypes.OpenAI.Responses.ResponseInputFile[] =
|
||||||
openaiFileIds.map((id) => ({
|
openaiFileIds.map((id) => ({
|
||||||
type: "input_file",
|
type: "input_file",
|
||||||
file_id: id,
|
file_id: id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const arr = Array.isArray(options.input) ? options.input : [];
|
const arr = Array.isArray(options.input) ? options.input : [];
|
||||||
arr.push({
|
arr.push({
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -120,6 +91,10 @@ export class OpenAIService {
|
|||||||
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 = (
|
||||||
@@ -128,13 +103,30 @@ export class OpenAIService {
|
|||||||
}
|
}
|
||||||
).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;
|
||||||
|
|
||||||
// Prefer `output_text` if present (SDK convenience)
|
// Prefer `output_text` if present (SDK convenience)
|
||||||
const maybeOutputText = openaiRaw.output_text;
|
const maybeOutputText = openaiRaw.output_text;
|
||||||
if (typeof maybeOutputText === "string" && maybeOutputText.length > 0) {
|
if (
|
||||||
|
typeof maybeOutputText === "string" &&
|
||||||
|
maybeOutputText.length > 0
|
||||||
|
) {
|
||||||
outputText = maybeOutputText;
|
outputText = maybeOutputText;
|
||||||
try {
|
try {
|
||||||
output = JSON.parse(maybeOutputText) as TOutput;
|
output = JSON.parse(maybeOutputText) as TOutput;
|
||||||
@@ -153,7 +145,6 @@ export class OpenAIService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
output,
|
output,
|
||||||
@@ -162,42 +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) {
|
||||||
@@ -209,13 +176,14 @@ export class OpenAIService {
|
|||||||
ids.push(created.id);
|
ids.push(created.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`OpenAI file upload failed: ${(e as Error)?.message ?? String(e)}`,
|
`OpenAI file upload failed: ${
|
||||||
|
(e as Error)?.message ?? String(e)
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeFilename(name: string): string {
|
private sanitizeFilename(name: string): string {
|
||||||
return name
|
return name
|
||||||
.normalize("NFD")
|
.normalize("NFD")
|
||||||
|
|||||||
@@ -47,3 +47,9 @@ export function sendError(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResponseMetadata extends Record<string, string | undefined> {
|
||||||
|
tabla?: string;
|
||||||
|
accion?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||||
"openai": "npm:openai@6.16.0",
|
|
||||||
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
||||||
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
// This enables autocomplete, go to definition, etc.
|
// This enables autocomplete, go to definition, etc.
|
||||||
|
|
||||||
// Setup type definitions for built-in Supabase Runtime APIs
|
// Setup type definitions for built-in Supabase Runtime APIs
|
||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import { corsHeaders } from "../_shared/cors.ts";
|
import { corsHeaders } from "../_shared/cors.ts";
|
||||||
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
||||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import type { Database, Json } from "../_shared/database.types.ts";
|
import type { Database, Json } from "../_shared/database.types.ts";
|
||||||
import type { AIGeneratePlanInput } from "./types.ts";
|
import type { AIGeneratePlanInput } from "./types.ts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -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.
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
{
|
{
|
||||||
"imports": {}
|
"imports": {
|
||||||
|
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||||
|
"openai": "npm:openai@6.16.0",
|
||||||
|
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
||||||
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,140 @@
|
|||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import { corsHeaders } from "../_shared/cors.ts";
|
import { corsHeaders } from "../_shared/cors.ts";
|
||||||
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
||||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
import { createClient } from "@supabase/supabase-js";
|
||||||
import type { Database, Json } from "../_shared/database.types.ts";
|
import type { Database, Json } from "../_shared/database.types.ts";
|
||||||
|
import { definicionesDeEstructurasDeColumnas } from "../_shared/estructuras.ts";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
OpenAIService,
|
||||||
|
type StructuredResponseOptions,
|
||||||
|
} from "../_shared/openai-service.ts";
|
||||||
|
|
||||||
console.log("Hello from Functions!");
|
type BeforeUnloadWithDetail = Event & { detail?: { reason?: unknown } };
|
||||||
|
|
||||||
Deno.serve(async (req) => {
|
addEventListener("beforeunload", (ev: BeforeUnloadWithDetail) => {
|
||||||
|
console.error(
|
||||||
|
"ALERTA: La función se va a apagar. Razón:",
|
||||||
|
ev?.detail?.reason,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const DatosUpdateSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string().uuid("id debe ser un UUID").optional(),
|
||||||
|
|
||||||
|
// campos de asignatura (nombres coinciden con columnas DB)
|
||||||
|
plan_estudio_id: z.string().uuid("plan_estudio_id debe ser un UUID"),
|
||||||
|
estructura_id: z.string().uuid("estructura_id debe ser un UUID").optional(),
|
||||||
|
nombre: z.string().min(1).optional(),
|
||||||
|
codigo: z.union([z.string().min(1), z.null()]).optional(),
|
||||||
|
tipo: z.union([z.string().min(1), z.null()]).optional(),
|
||||||
|
creditos: z.number().positive().optional(),
|
||||||
|
horas_academicas: z.number().int().nonnegative().nullable().optional(),
|
||||||
|
horas_independientes: z.number().int().nonnegative().nullable().optional(),
|
||||||
|
numero_ciclo: z.number().int().positive().nullable().optional(),
|
||||||
|
linea_plan_id: z.string().uuid().nullable().optional(),
|
||||||
|
orden_celda: z.number().int().nonnegative().nullable().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const IAConfigSchema = z
|
||||||
|
.object({
|
||||||
|
descripcionEnfoqueAcademico: z.string().optional(),
|
||||||
|
instruccionesAdicionalesIA: z.string().optional(),
|
||||||
|
archivosAdjuntos: z.array(z.string().min(1)).optional().default([]),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.default({});
|
||||||
|
|
||||||
|
const UnifiedJsonSchema = z
|
||||||
|
.object({
|
||||||
|
datosUpdate: DatosUpdateSchema,
|
||||||
|
iaConfig: IAConfigSchema,
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
// Si no hay id, es un insert => necesitamos campos mínimos
|
||||||
|
if (!val.datosUpdate.id) {
|
||||||
|
if (!val.datosUpdate.estructura_id) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["datosUpdate", "estructura_id"],
|
||||||
|
message: "estructura_id es requerido para crear",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!val.datosUpdate.nombre) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["datosUpdate", "nombre"],
|
||||||
|
message: "nombre es requerido para crear",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (val.datosUpdate.creditos === undefined) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["datosUpdate", "creditos"],
|
||||||
|
message: "creditos es requerido para crear",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type EdgeAIGenerateSubjectUnifiedInput = z.infer<typeof UnifiedJsonSchema>;
|
||||||
|
|
||||||
|
type AsignaturaBaseSeleccionada = {
|
||||||
|
id: string;
|
||||||
|
plan_estudio_id: string;
|
||||||
|
estructura_id: string | null;
|
||||||
|
nombre: string;
|
||||||
|
codigo: string | null;
|
||||||
|
tipo: Database["public"]["Tables"]["asignaturas"]["Row"]["tipo"];
|
||||||
|
creditos: number;
|
||||||
|
horas_academicas: number | null;
|
||||||
|
horas_independientes: number | null;
|
||||||
|
numero_ciclo: number | null;
|
||||||
|
linea_plan_id: string | null;
|
||||||
|
orden_celda: number | null;
|
||||||
|
estado: Database["public"]["Enums"]["estado_asignatura"];
|
||||||
|
};
|
||||||
|
|
||||||
|
function withColumnDefsAndRefs(
|
||||||
|
schemaDef: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const nextSchema: Record<string, unknown> = {
|
||||||
|
...schemaDef,
|
||||||
|
$defs: definicionesDeEstructurasDeColumnas,
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = nextSchema["properties"];
|
||||||
|
if (!props || typeof props !== "object" || Array.isArray(props)) {
|
||||||
|
return nextSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextProps: Record<string, unknown> = {
|
||||||
|
...props as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
for (const [key, value] of Object.entries(nextProps)) {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
||||||
|
const xColumn = (value as Record<string, unknown>)["x-column"];
|
||||||
|
if (typeof xColumn !== "string" || !xColumn.length) continue;
|
||||||
|
nextProps[key] = { $ref: `#/$defs/${xColumn}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSchema["properties"] = nextProps;
|
||||||
|
return nextSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
console.log(
|
console.log(
|
||||||
@@ -18,6 +146,53 @@ Deno.serve(async (req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const method = req.method;
|
||||||
|
if (method !== "POST") {
|
||||||
|
console.error(
|
||||||
|
`[${
|
||||||
|
new Date().toISOString()
|
||||||
|
}][${functionName}]: Invalid method: ${method}`,
|
||||||
|
);
|
||||||
|
throw new HttpError(
|
||||||
|
405,
|
||||||
|
"Método no permitido.",
|
||||||
|
"METHOD_NOT_ALLOWED",
|
||||||
|
{ method },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeaderRaw = req.headers.get("Authorization") ??
|
||||||
|
req.headers.get("authorization");
|
||||||
|
if (!authHeaderRaw) {
|
||||||
|
console.error(
|
||||||
|
`[${
|
||||||
|
new Date().toISOString()
|
||||||
|
}][${functionName}]: Missing Authorization header`,
|
||||||
|
);
|
||||||
|
throw new HttpError(
|
||||||
|
401,
|
||||||
|
"No autorizado.",
|
||||||
|
"UNAUTHORIZED",
|
||||||
|
{ reason: "missing_authorization_header" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = (req.headers.get("content-type") || "").toLowerCase();
|
||||||
|
const isJson = contentType.includes("application/json");
|
||||||
|
if (!isJson) {
|
||||||
|
console.error(
|
||||||
|
`[${
|
||||||
|
new Date().toISOString()
|
||||||
|
}][${functionName}]: Unsupported content type: ${contentType}`,
|
||||||
|
);
|
||||||
|
throw new HttpError(
|
||||||
|
415,
|
||||||
|
"Content-Type no soportado.",
|
||||||
|
"UNSUPPORTED_MEDIA_TYPE",
|
||||||
|
{ contentType, expected: "application/json" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
||||||
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
||||||
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
||||||
@@ -59,20 +234,340 @@ Deno.serve(async (req) => {
|
|||||||
SERVICE_ROLE_KEY,
|
SERVICE_ROLE_KEY,
|
||||||
);
|
);
|
||||||
|
|
||||||
// INICIO DE CODIGO PARA DEBBUGGING
|
// Model names (override via environment variables)
|
||||||
|
const AI_GENERATE_SUBJECT_UPDATE_MODELO = Deno.env.get(
|
||||||
|
"AI_GENERATE_SUBJECT_UPDATE_MODELO",
|
||||||
|
) ?? "gpt-5-nano";
|
||||||
|
const AI_GENERATE_SUBJECT_INSERT_MODELO = Deno.env.get(
|
||||||
|
"AI_GENERATE_SUBJECT_INSERT_MODELO",
|
||||||
|
) ?? "gpt-5-nano";
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Unified JSON create/update flow (background)
|
||||||
|
// -----------------------------
|
||||||
|
let rawBody: unknown;
|
||||||
|
try {
|
||||||
|
rawBody = await req.json();
|
||||||
|
} catch (e) {
|
||||||
|
throw new HttpError(
|
||||||
|
400,
|
||||||
|
"Body JSON inválido.",
|
||||||
|
"INVALID_JSON",
|
||||||
|
{ cause: e },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = UnifiedJsonSchema.safeParse(rawBody);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
throw new HttpError(
|
||||||
|
422,
|
||||||
|
formatZodIssues(parsedBody.error.issues),
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
parsedBody.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: EdgeAIGenerateSubjectUnifiedInput = parsedBody.data;
|
||||||
|
const { datosUpdate, iaConfig } = payload;
|
||||||
|
const isUpdate = Boolean(datosUpdate.id);
|
||||||
|
|
||||||
|
const svc = OpenAIService.fromEnv();
|
||||||
|
if (!(svc instanceof OpenAIService)) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Configuración del servidor incompleta.",
|
||||||
|
"OPENAI_MISCONFIGURED",
|
||||||
|
svc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve fields (merge for update)
|
||||||
|
const selectCols =
|
||||||
|
"id,plan_estudio_id,estructura_id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,numero_ciclo,linea_plan_id,orden_celda,estado";
|
||||||
|
|
||||||
|
let asignaturaBase: AsignaturaBaseSeleccionada | null = null;
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
const { data, error } = await supabaseService
|
||||||
|
.from("asignaturas")
|
||||||
|
.select(selectCols)
|
||||||
|
.eq("id", datosUpdate.id as string)
|
||||||
|
.single();
|
||||||
|
if (error) {
|
||||||
|
const maybeCode = (error as { code?: string }).code;
|
||||||
|
if (maybeCode === "PGRST116") {
|
||||||
|
throw new HttpError(
|
||||||
|
404,
|
||||||
|
"No se encontró la asignatura.",
|
||||||
|
"NOT_FOUND",
|
||||||
|
{ table: "asignaturas", id: datosUpdate.id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"No se pudo obtener la asignatura.",
|
||||||
|
"SUPABASE_QUERY_FAILED",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
asignaturaBase = data as unknown as AsignaturaBaseSeleccionada;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = {
|
||||||
|
id: (datosUpdate.id ?? asignaturaBase?.id ?? null) as string | null,
|
||||||
|
plan_estudio_id: datosUpdate.plan_estudio_id ??
|
||||||
|
asignaturaBase?.plan_estudio_id,
|
||||||
|
estructura_id: datosUpdate.estructura_id ??
|
||||||
|
asignaturaBase?.estructura_id ?? null,
|
||||||
|
nombre: datosUpdate.nombre ?? asignaturaBase?.nombre ?? null,
|
||||||
|
codigo: datosUpdate.codigo ?? asignaturaBase?.codigo ?? null,
|
||||||
|
tipo: datosUpdate.tipo ?? asignaturaBase?.tipo ?? null,
|
||||||
|
creditos: datosUpdate.creditos ?? asignaturaBase?.creditos ?? null,
|
||||||
|
horas_academicas: datosUpdate.horas_academicas ??
|
||||||
|
asignaturaBase?.horas_academicas ?? null,
|
||||||
|
horas_independientes: datosUpdate.horas_independientes ??
|
||||||
|
asignaturaBase?.horas_independientes ?? null,
|
||||||
|
numero_ciclo: datosUpdate.numero_ciclo ?? asignaturaBase?.numero_ciclo ??
|
||||||
|
null,
|
||||||
|
linea_plan_id: datosUpdate.linea_plan_id ??
|
||||||
|
asignaturaBase?.linea_plan_id ?? null,
|
||||||
|
orden_celda: datosUpdate.orden_celda ?? asignaturaBase?.orden_celda ??
|
||||||
|
null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!resolved.estructura_id) {
|
||||||
|
throw new HttpError(
|
||||||
|
422,
|
||||||
|
"estructura_id es requerido.",
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
{ estructura_id: resolved.estructura_id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!resolved.nombre) {
|
||||||
|
throw new HttpError(
|
||||||
|
422,
|
||||||
|
"nombre es requerido.",
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
{ nombre: resolved.nombre },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (resolved.creditos == null) {
|
||||||
|
throw new HttpError(
|
||||||
|
422,
|
||||||
|
"creditos es requerido.",
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
{ creditos: resolved.creditos },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear/actualizar stub en estado 'generando'
|
||||||
|
let asignaturaId: string;
|
||||||
|
if (isUpdate) {
|
||||||
|
const updatePatch: Database["public"]["Tables"]["asignaturas"]["Update"] =
|
||||||
|
{
|
||||||
|
estado: "generando",
|
||||||
|
tipo_origen: "IA",
|
||||||
|
plan_estudio_id: resolved.plan_estudio_id,
|
||||||
|
estructura_id: resolved.estructura_id,
|
||||||
|
nombre: resolved.nombre,
|
||||||
|
codigo: resolved.codigo,
|
||||||
|
creditos: resolved.creditos,
|
||||||
|
horas_academicas: resolved.horas_academicas,
|
||||||
|
horas_independientes: resolved.horas_independientes,
|
||||||
|
numero_ciclo: resolved.numero_ciclo,
|
||||||
|
linea_plan_id: resolved.linea_plan_id,
|
||||||
|
orden_celda: resolved.orden_celda,
|
||||||
|
tipo: resolved
|
||||||
|
.tipo as Database["public"]["Tables"]["asignaturas"]["Update"][
|
||||||
|
"tipo"
|
||||||
|
],
|
||||||
|
meta_origen: {
|
||||||
|
generado_por: "ai_generate_subject_unified",
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoqueAcademico:
|
||||||
|
iaConfig.descripcionEnfoqueAcademico ?? null,
|
||||||
|
instruccionesAdicionalesIA: iaConfig.instruccionesAdicionalesIA ??
|
||||||
|
null,
|
||||||
|
archivosAdjuntos: iaConfig.archivosAdjuntos ?? [],
|
||||||
|
},
|
||||||
|
} as unknown as Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = await supabaseService
|
||||||
|
.from("asignaturas")
|
||||||
|
.update(updatePatch)
|
||||||
|
.eq("id", resolved.id as string)
|
||||||
|
.select("id")
|
||||||
|
.single();
|
||||||
|
if (error) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"No se pudo actualizar la asignatura (stub).",
|
||||||
|
"SUPABASE_UPDATE_FAILED",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
asignaturaId = data.id;
|
||||||
|
} else {
|
||||||
|
const insertPatch: Database["public"]["Tables"]["asignaturas"]["Insert"] =
|
||||||
|
{
|
||||||
|
plan_estudio_id: resolved.plan_estudio_id as string,
|
||||||
|
estructura_id: resolved.estructura_id as string,
|
||||||
|
nombre: resolved.nombre as string,
|
||||||
|
codigo: resolved.codigo ?? null,
|
||||||
|
tipo: (resolved.tipo ?? undefined) as Database["public"]["Tables"][
|
||||||
|
"asignaturas"
|
||||||
|
]["Insert"]["tipo"],
|
||||||
|
creditos: resolved.creditos as number,
|
||||||
|
horas_academicas: resolved.horas_academicas ?? null,
|
||||||
|
horas_independientes: resolved.horas_independientes ?? null,
|
||||||
|
numero_ciclo: resolved.numero_ciclo ?? null,
|
||||||
|
linea_plan_id: resolved.linea_plan_id ?? null,
|
||||||
|
orden_celda: resolved.orden_celda ?? null,
|
||||||
|
estado: "generando",
|
||||||
|
tipo_origen: "IA",
|
||||||
|
meta_origen: {
|
||||||
|
generado_por: "ai_generate_subject_unified",
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoqueAcademico:
|
||||||
|
iaConfig.descripcionEnfoqueAcademico ?? null,
|
||||||
|
instruccionesAdicionalesIA: iaConfig.instruccionesAdicionalesIA ??
|
||||||
|
null,
|
||||||
|
archivosAdjuntos: iaConfig.archivosAdjuntos ?? [],
|
||||||
|
},
|
||||||
|
} as unknown as Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = await supabaseService
|
||||||
|
.from("asignaturas")
|
||||||
|
.insert(insertPatch)
|
||||||
|
.select("id")
|
||||||
|
.single();
|
||||||
|
if (error) {
|
||||||
|
const maybeCode = (error as { code?: string }).code;
|
||||||
|
const status = maybeCode ? 409 : 500;
|
||||||
|
throw new HttpError(
|
||||||
|
status,
|
||||||
|
"No se pudo crear la asignatura (stub).",
|
||||||
|
"SUPABASE_INSERT_FAILED",
|
||||||
|
{ ...error, code: maybeCode },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
asignaturaId = data.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: estructura, error: estructuraError } = await supabaseService
|
||||||
|
.from("estructuras_asignatura")
|
||||||
|
.select("id,nombre,definicion")
|
||||||
|
.eq("id", resolved.estructura_id)
|
||||||
|
.single();
|
||||||
|
if (estructuraError) {
|
||||||
|
const maybeCode = (estructuraError as { code?: string }).code;
|
||||||
|
if (maybeCode === "PGRST116") {
|
||||||
|
throw new HttpError(
|
||||||
|
404,
|
||||||
|
"No se encontró la estructura de la asignatura.",
|
||||||
|
"NOT_FOUND",
|
||||||
|
{ table: "estructuras_asignatura", id: resolved.estructura_id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"No se pudo obtener la estructura de la asignatura.",
|
||||||
|
"SUPABASE_QUERY_FAILED",
|
||||||
|
estructuraError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPrompt =
|
||||||
|
`Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el JSON Schema proporcionado.`;
|
||||||
|
|
||||||
|
const archivosAdjuntosTexto = (iaConfig.archivosAdjuntos ?? []).length
|
||||||
|
? `\n- Rutas de archivos adjuntos (referencia): ${
|
||||||
|
(iaConfig.archivosAdjuntos ?? []).join(", ")
|
||||||
|
}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const userPrompt =
|
||||||
|
`Genera un borrador completo completo de una ASIGNATURA con base en lo siguiente:\n` +
|
||||||
|
`- Nombre de la asignatura: ${resolved.nombre}\n` +
|
||||||
|
`- Código (clave de la asignatura): ${
|
||||||
|
resolved.codigo ?? "(no especificado)"
|
||||||
|
}\n` +
|
||||||
|
`- Tipo: ${resolved.tipo ?? "(no especificado)"}\n` +
|
||||||
|
`- Número de créditos: ${resolved.creditos}\n` +
|
||||||
|
`- Horas académicas: ${
|
||||||
|
resolved.horas_academicas ?? "(no especificado)"
|
||||||
|
}\n` +
|
||||||
|
`- Horas independientes: ${
|
||||||
|
resolved.horas_independientes ?? "(no especificado)"
|
||||||
|
}\n` +
|
||||||
|
`- Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${
|
||||||
|
iaConfig.descripcionEnfoqueAcademico ?? "(ninguna)"
|
||||||
|
}\n` +
|
||||||
|
`- Instrucciones adicionales (sobre el formato de la respuesta generada): ${
|
||||||
|
iaConfig.instruccionesAdicionalesIA ?? "(ninguna)"
|
||||||
|
}` +
|
||||||
|
archivosAdjuntosTexto +
|
||||||
|
`\n\nREGLAS ESTRICTAS MATEMÁTICAS:\n` +
|
||||||
|
`- Si generas el 'contenido_tematico', la suma total de las 'horasEstimadas' de todos los temas en todas las unidades DEBE coincidir exactamente con el total de Horas académicas indicadas arriba (${
|
||||||
|
resolved.horas_academicas ?? 0
|
||||||
|
}). No te pases ni te falten horas.\n` +
|
||||||
|
`- Si generas los 'criterios_de_evaluacion', la suma total de los 'porcentajes' de todos los criterios DEBE ser exactamente 100%. No te pases ni te falten porcentajes.`;
|
||||||
|
|
||||||
|
const schemaDef: Record<string, unknown> =
|
||||||
|
typeof estructura?.definicion === "object" &&
|
||||||
|
estructura?.definicion !== null
|
||||||
|
? (estructura.definicion as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const schemaForAI = withColumnDefsAndRefs(schemaDef);
|
||||||
|
|
||||||
|
const aiStructuredPayload: StructuredResponseOptions = {
|
||||||
|
model: isUpdate
|
||||||
|
? AI_GENERATE_SUBJECT_UPDATE_MODELO
|
||||||
|
: AI_GENERATE_SUBJECT_INSERT_MODELO,
|
||||||
|
background: true,
|
||||||
|
metadata: {
|
||||||
|
tabla: "asignaturas",
|
||||||
|
accion: isUpdate ? "actualizar" : "crear",
|
||||||
|
id: asignaturaId,
|
||||||
|
},
|
||||||
|
input: [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{ role: "user", content: userPrompt },
|
||||||
|
],
|
||||||
|
text: {
|
||||||
|
format: {
|
||||||
|
type: "json_schema",
|
||||||
|
name: "asignatura_contenido_tematico",
|
||||||
|
schema: schemaForAI,
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiResult = await svc.createStructuredResponse(aiStructuredPayload);
|
||||||
|
if (!aiResult.ok) {
|
||||||
|
const status = aiResult.code === "MissingEnv" ? 500 : 502;
|
||||||
|
throw new HttpError(
|
||||||
|
status,
|
||||||
|
"No se pudo iniciar la generación de la asignatura con IA.",
|
||||||
|
"OPENAI_REQUEST_FAILED",
|
||||||
|
aiResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${
|
`[${
|
||||||
new Date().toISOString()
|
new Date().toISOString()
|
||||||
}][${functionName}]: Request processed successfully`,
|
}][${functionName}]: Subject generation started in background`,
|
||||||
);
|
);
|
||||||
const { data: asignatura_debug, error: asignatura_error } =
|
return sendSuccess({
|
||||||
await supabaseService
|
id: asignaturaId,
|
||||||
.from("asignaturas")
|
estado: "generando",
|
||||||
.select("*")
|
openai: { responseId: aiResult.responseId ?? null },
|
||||||
.eq("id", "9d4dda6a-488f-428a-8a07-38081592a641")
|
});
|
||||||
.single();
|
|
||||||
return sendSuccess(asignatura_debug);
|
|
||||||
// FIN DE CODIGO PARA DEBBUGGING
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Tables } from "../_shared/database.types.ts";
|
||||||
|
|
||||||
|
export type AIGenerateSubjectInput = {
|
||||||
|
datosUpdate: {
|
||||||
|
id?: Tables<"asignaturas">["id"]; // si viene, es update; si no, es insert
|
||||||
|
plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
|
||||||
|
estructura_id?: Tables<"asignaturas">["estructura_id"] | null;
|
||||||
|
|
||||||
|
nombre?: Tables<"asignaturas">["nombre"];
|
||||||
|
codigo?: Tables<"asignaturas">["codigo"];
|
||||||
|
tipo?: Tables<"asignaturas">["tipo"] | null;
|
||||||
|
creditos?: Tables<"asignaturas">["creditos"] | null;
|
||||||
|
horas_academicas?: Tables<"asignaturas">["horas_academicas"] | null;
|
||||||
|
horas_independientes?: Tables<"asignaturas">["horas_independientes"] | null;
|
||||||
|
numero_ciclo?: Tables<"asignaturas">["numero_ciclo"] | null;
|
||||||
|
linea_plan_id?: Tables<"asignaturas">["linea_plan_id"] | null;
|
||||||
|
orden_celda?: Tables<"asignaturas">["orden_celda"] | null;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoqueAcademico?: string;
|
||||||
|
instruccionesAdicionalesIA?: string;
|
||||||
|
archivosAdjuntos?: string[]; // rutas (no File)
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1 +0,0 @@
|
|||||||
verify_jwt = false
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"imports": {
|
|
||||||
"openai": "npm:openai@6.16.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Function deprecated. This endpoint was replaced by shared module `_shared/openai-service.ts`.
|
|
||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
||||||
|
|
||||||
const json = (body: unknown, status = 200) =>
|
|
||||||
new Response(JSON.stringify(body), {
|
|
||||||
status,
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json; charset=utf-8",
|
|
||||||
"access-control-allow-origin": "*",
|
|
||||||
"access-control-allow-headers": "*",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.serve((req) => {
|
|
||||||
if (req.method === "OPTIONS") return json({ ok: true });
|
|
||||||
return json(
|
|
||||||
{
|
|
||||||
ok: false,
|
|
||||||
error:
|
|
||||||
"This endpoint is deprecated. Use the shared module _shared/openai-service.ts from your functions.",
|
|
||||||
},
|
|
||||||
410,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
// supabase/functions/ai_generate_plan/index.ts
|
|
||||||
/// <reference lib="deno.window" />
|
|
||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
||||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
|
||||||
|
|
||||||
const corsHeaders = {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Headers":
|
|
||||||
"authorization, x-client-info, apikey, content-type",
|
|
||||||
};
|
|
||||||
|
|
||||||
function json(body: unknown, status = 200) {
|
|
||||||
return new Response(JSON.stringify(body, null, 2), {
|
|
||||||
status,
|
|
||||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ----------------- LOGGING HELPERS (diagnóstico) ----------------- */
|
|
||||||
function nowIso() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function redactAuthHeader(v: string | null) {
|
|
||||||
if (!v) return null;
|
|
||||||
const parts = v.split(" ");
|
|
||||||
if (parts.length >= 2) return `${parts[0]} [REDACTED]`;
|
|
||||||
return "[REDACTED]";
|
|
||||||
}
|
|
||||||
|
|
||||||
function safePreview(val: unknown, maxLen = 1400) {
|
|
||||||
try {
|
|
||||||
const jsonStr = JSON.stringify(val, (k, v) => {
|
|
||||||
if (typeof v === "string") {
|
|
||||||
if (v.length > 800) return v.slice(0, 800) + "…[TRUNCATED]";
|
|
||||||
if (v.startsWith("data:") && v.includes(";base64,")) return "[DATA_URL_REDACTED]";
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
if (!jsonStr) return String(val);
|
|
||||||
return jsonStr.length > maxLen ? jsonStr.slice(0, maxLen) + "…[TRUNCATED]" : jsonStr;
|
|
||||||
} catch {
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function headersSnapshot(req: Request) {
|
|
||||||
const h: Record<string, string> = {};
|
|
||||||
for (const [k, v] of req.headers.entries()) {
|
|
||||||
if (k.toLowerCase() === "authorization") h[k] = redactAuthHeader(v) ?? "";
|
|
||||||
else h[k] = v;
|
|
||||||
}
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNivel(raw: string): string {
|
|
||||||
const s = (raw ?? "").trim().toLowerCase();
|
|
||||||
if (!s) return "Otro";
|
|
||||||
if (s === "licenciatura" || s === "lic") return "Licenciatura";
|
|
||||||
if (s === "maestria" || s === "maestría" || s === "msc") return "Maestría";
|
|
||||||
if (s === "doctorado" || s === "phd") return "Doctorado";
|
|
||||||
if (s === "especialidad") return "Especialidad";
|
|
||||||
if (s === "diplomado") return "Diplomado";
|
|
||||||
return "Otro";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTipoCiclo(raw: string): string {
|
|
||||||
const s = (raw ?? "").trim().toLowerCase();
|
|
||||||
if (!s) return "Otro";
|
|
||||||
if (s === "semestre" || s === "semestres" || s === "sem" || s === "semestre(s)" ||
|
|
||||||
s === "semestre" || s === "semestre" || s === "semestre") return "Semestre";
|
|
||||||
if (s === "cuatrimestre" || s === "cuatrimestres" || s === "cuatri") return "Cuatrimestre";
|
|
||||||
if (s === "trimestre" || s === "trimestres" || s === "trim") return "Trimestre";
|
|
||||||
// si viene en mayúsculas tipo frontend
|
|
||||||
if (s === "semestre" || s === "semestre".toLowerCase()) return "Semestre";
|
|
||||||
if (s === "cuatrimestre" || s === "cuatrimestre".toLowerCase()) return "Cuatrimestre";
|
|
||||||
if (s === "trimestre" || s === "trimestre".toLowerCase()) return "Trimestre";
|
|
||||||
if (s === "otro") return "Otro";
|
|
||||||
// variantes comunes
|
|
||||||
if (s === "semestre".toLowerCase()) return "Semestre";
|
|
||||||
if (s === "cuatrimestre".toLowerCase()) return "Cuatrimestre";
|
|
||||||
if (s === "trimestre".toLowerCase()) return "Trimestre";
|
|
||||||
// si llega como SEMESTRE / CUATRIMESTRE / TRIMESTRE
|
|
||||||
if (s === "semestre".toLowerCase()) return "Semestre";
|
|
||||||
if (s === "cuatrimestre".toLowerCase()) return "Cuatrimestre";
|
|
||||||
if (s === "trimestre".toLowerCase()) return "Trimestre";
|
|
||||||
// fallback
|
|
||||||
return "Otro";
|
|
||||||
}
|
|
||||||
|
|
||||||
function suggestedWeeksFor(tipoCiclo: string) {
|
|
||||||
switch (tipoCiclo) {
|
|
||||||
case "Semestre":
|
|
||||||
return "16";
|
|
||||||
case "Cuatrimestre":
|
|
||||||
return "16";
|
|
||||||
case "Trimestre":
|
|
||||||
return "12";
|
|
||||||
default:
|
|
||||||
return "16";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPrompt(params: {
|
|
||||||
datosBasicos: any;
|
|
||||||
carrera?: any;
|
|
||||||
estructura?: any;
|
|
||||||
iaConfig?: any;
|
|
||||||
}) {
|
|
||||||
const { datosBasicos, carrera, estructura, iaConfig } = params;
|
|
||||||
|
|
||||||
const nombrePlan = String(datosBasicos?.nombrePlan ?? "").trim();
|
|
||||||
const nivel = normalizeNivel(String(datosBasicos?.nivel ?? ""));
|
|
||||||
const tipoCiclo = normalizeTipoCiclo(String(datosBasicos?.tipoCiclo ?? ""));
|
|
||||||
const numCiclos = Number(datosBasicos?.numCiclos ?? 0) || 0;
|
|
||||||
|
|
||||||
const semanas = suggestedWeeksFor(tipoCiclo);
|
|
||||||
|
|
||||||
const carreraTxt = carrera
|
|
||||||
? `Carrera: ${carrera?.nombre ?? ""} (id: ${carrera?.id ?? ""})\nFacultad: ${carrera?.facultades?.nombre ?? carrera?.facultad?.nombre ?? ""}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const system = [
|
|
||||||
"Eres un experto en diseño curricular (México) y redacción institucional.",
|
|
||||||
"Debes generar SOLO JSON válido que cumpla EXACTAMENTE el schema (sin campos extra).",
|
|
||||||
"Usa español neutro y redacción formal. Si un campo es opcional y no aplica, usa null.",
|
|
||||||
"Respeta enumeraciones (p.ej. modalidad_educativa, diseno_curricular).",
|
|
||||||
"Asegura consistencia interna entre nivel, fines, perfiles y área de estudio.",
|
|
||||||
"La carga_horaria_a_la_semana NO debe exceder 50.",
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
const user = [
|
|
||||||
"Genera un borrador completo del PLAN DE ESTUDIOS con base en lo siguiente:",
|
|
||||||
"",
|
|
||||||
"== Datos básicos (NO los cambies) ==",
|
|
||||||
`- nivel: ${nivel}`,
|
|
||||||
`- nombre: ${nombrePlan}`,
|
|
||||||
`- tipo de ciclo: ${tipoCiclo}`,
|
|
||||||
`- número de ciclos: ${numCiclos}`,
|
|
||||||
`- duración sugerida del ciclo escolar (semanas efectivas): ${semanas}`,
|
|
||||||
"",
|
|
||||||
carreraTxt ? `== Contexto institucional ==\n${carreraTxt}\n` : "",
|
|
||||||
"== Enfoque / Población / Notas ==",
|
|
||||||
`- descripcionEnfoque: ${iaConfig?.descripcionEnfoque ?? ""}`,
|
|
||||||
`- notasAdicionales: ${iaConfig?.notasAdicionales ?? ""}`,
|
|
||||||
"",
|
|
||||||
"== Reglas adicionales ==",
|
|
||||||
"- Alinea: fines_de_aprendizaje_o_formacion ↔ perfil_de_egreso ↔ area_de_estudio.",
|
|
||||||
`- total_de_ciclos_del_plan_de_estudios debe ser \"${String(numCiclos)}\" (string).`,
|
|
||||||
`- duracion_del_ciclo_escolar debe ser \"${semanas}\" (string).`,
|
|
||||||
"- diseno_curricular: elige 'Rígido' o 'Flexible' coherente con la administración.",
|
|
||||||
"- Si modalidad_educativa es 'No escolarizada' o 'Mixta', llena justificacion_de_la_propuesta_curricular; si es 'Escolar', puede ser null.",
|
|
||||||
"- clave_del_plan_de_estudios: genera una clave administrativa verosímil (p.ej. 2026-XXX-YYY).",
|
|
||||||
"",
|
|
||||||
"Entrega únicamente el JSON final (sin Markdown, sin explicación).",
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
return { system, user, computed: { nivel, tipoCiclo, numCiclos, semanas, nombrePlan } };
|
|
||||||
}
|
|
||||||
|
|
||||||
Deno.serve(async (req) => {
|
|
||||||
const cid = crypto.randomUUID();
|
|
||||||
const t0 = performance.now();
|
|
||||||
|
|
||||||
const log = (msg: string, extra?: unknown) => {
|
|
||||||
if (extra !== undefined) {
|
|
||||||
console.log(`[${nowIso()}][ai_generate_plan][${cid}] ${msg}`, safePreview(extra));
|
|
||||||
} else {
|
|
||||||
console.log(`[${nowIso()}][ai_generate_plan][${cid}] ${msg}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const warn = (msg: string, extra?: unknown) => {
|
|
||||||
if (extra !== undefined) {
|
|
||||||
console.warn(`[${nowIso()}][ai_generate_plan][${cid}] ⚠️ ${msg}`, safePreview(extra));
|
|
||||||
} else {
|
|
||||||
console.warn(`[${nowIso()}][ai_generate_plan][${cid}] ⚠️ ${msg}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const errlog = (msg: string, extra?: unknown) => {
|
|
||||||
if (extra !== undefined) {
|
|
||||||
console.error(`[${nowIso()}][ai_generate_plan][${cid}] ❌ ${msg}`, safePreview(extra));
|
|
||||||
} else {
|
|
||||||
console.error(`[${nowIso()}][ai_generate_plan][${cid}] ❌ ${msg}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
|
||||||
log("OPTIONS preflight ✅", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: headersSnapshot(req),
|
|
||||||
});
|
|
||||||
return new Response(null, { status: 204, headers: corsHeaders });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
warn("Method not allowed", { method: req.method });
|
|
||||||
return json({ ok: false, error: "Method not allowed", cid }, 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
log("START 🚀", { method: req.method, url: req.url });
|
|
||||||
log("Headers snapshot 🧾", headersSnapshot(req));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1) Auth requerido ✅
|
|
||||||
log("Step 1: Auth check 🔐");
|
|
||||||
const authHeaderRaw = req.headers.get("Authorization") ??
|
|
||||||
req.headers.get("authorization");
|
|
||||||
log("Authorization header present?", {
|
|
||||||
present: Boolean(authHeaderRaw),
|
|
||||||
value: redactAuthHeader(authHeaderRaw),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!authHeaderRaw) {
|
|
||||||
warn("Missing Authorization header");
|
|
||||||
return json({ ok: false, error: "Missing Authorization header", cid }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
|
||||||
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
|
||||||
log("Env presence (Supabase) 🌍", {
|
|
||||||
hasSUPABASE_URL: Boolean(SUPABASE_URL),
|
|
||||||
hasSUPABASE_ANON_KEY: Boolean(SUPABASE_ANON_KEY),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
||||||
errlog("Missing SUPABASE_URL / SUPABASE_ANON_KEY");
|
|
||||||
return json(
|
|
||||||
{ ok: false, error: "Missing SUPABASE_URL / SUPABASE_ANON_KEY", cid },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUser = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
||||||
global: { headers: { Authorization: authHeaderRaw } },
|
|
||||||
});
|
|
||||||
|
|
||||||
const tAuth = performance.now();
|
|
||||||
const { data: userData, error: userErr } = await supabaseUser.auth.getUser();
|
|
||||||
log("Supabase auth.getUser() done ⏱️", { ms: Math.round(performance.now() - tAuth) });
|
|
||||||
|
|
||||||
if (userErr || !userData?.user) {
|
|
||||||
warn("Invalid token", { userErr: userErr?.message ?? userErr ?? null });
|
|
||||||
return json({ ok: false, error: "Invalid token", cid }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = userData.user;
|
|
||||||
log("Authenticated user ✅", { userId: user.id, email: user.email ?? null });
|
|
||||||
|
|
||||||
const SERVICE_ROLE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
||||||
log("Env presence (service role) 🗝️", { hasSERVICE_ROLE: Boolean(SERVICE_ROLE) });
|
|
||||||
|
|
||||||
const supabaseAdmin = SERVICE_ROLE
|
|
||||||
? createClient(SUPABASE_URL, SERVICE_ROLE)
|
|
||||||
: supabaseUser;
|
|
||||||
|
|
||||||
log("Supabase client selected 🧩", { adminMode: Boolean(SERVICE_ROLE) });
|
|
||||||
|
|
||||||
// 2) Parse body 🧾
|
|
||||||
log("Step 2: Parse body 🧾");
|
|
||||||
let payload: any = {};
|
|
||||||
const tParse = performance.now();
|
|
||||||
try {
|
|
||||||
payload = await req.json();
|
|
||||||
} catch (e) {
|
|
||||||
errlog("Invalid JSON body", { error: (e as Error)?.message ?? String(e) });
|
|
||||||
return json({ ok: false, error: "Invalid JSON body", cid }, 400);
|
|
||||||
}
|
|
||||||
log("Body parsed ⏱️", { ms: Math.round(performance.now() - tParse) });
|
|
||||||
log("Payload preview", { keys: Object.keys(payload ?? {}), payload: payload ? { ...payload, iaConfig: undefined } : null });
|
|
||||||
|
|
||||||
const datosBasicos = payload?.datosBasicos ?? payload?.input?.datosBasicos;
|
|
||||||
const iaConfig = payload?.iaConfig ?? payload?.input?.iaConfig ?? {};
|
|
||||||
|
|
||||||
if (!datosBasicos?.nombrePlan || !datosBasicos?.carreraId || !datosBasicos?.nivel || !datosBasicos?.tipoCiclo || !datosBasicos?.numCiclos) {
|
|
||||||
warn("Missing required datosBasicos fields", { datosBasicos });
|
|
||||||
return json({
|
|
||||||
ok: false,
|
|
||||||
error: "Missing required fields in datosBasicos (nombrePlan,carreraId,nivel,tipoCiclo,numCiclos).",
|
|
||||||
cid,
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Asegura usuarios_app (por FK en logs) 👤
|
|
||||||
log("Step 3: Ensure usuarios_app row 👤");
|
|
||||||
const tUserUpsert = performance.now();
|
|
||||||
const upsertUser = {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email ?? null,
|
|
||||||
nombre_completo: (user.user_metadata as any)?.full_name ??
|
|
||||||
(user.user_metadata as any)?.name ?? null,
|
|
||||||
};
|
|
||||||
const { error: upsertErr } = await supabaseAdmin
|
|
||||||
.from("usuarios_app")
|
|
||||||
.upsert(upsertUser, { onConflict: "id" });
|
|
||||||
log("usuarios_app upsert done ⏱️", { ms: Math.round(performance.now() - tUserUpsert), ok: !upsertErr });
|
|
||||||
if (upsertErr) {
|
|
||||||
errlog("usuarios_app upsert failed", { message: upsertErr.message });
|
|
||||||
return json({ ok: false, error: "usuarios_app upsert failed", details: upsertErr.message, cid }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Trae carrera + facultad (contexto prompt) 🎓
|
|
||||||
log("Step 4: Fetch carrera/facultad 🎓");
|
|
||||||
const carreraId = datosBasicos.carreraId;
|
|
||||||
const tCarr = performance.now();
|
|
||||||
const { data: carrera, error: carreraErr } = await supabaseAdmin
|
|
||||||
.from("carreras")
|
|
||||||
.select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)")
|
|
||||||
.eq("id", carreraId)
|
|
||||||
.maybeSingle();
|
|
||||||
log("Carrera fetch done ⏱️", { ms: Math.round(performance.now() - tCarr), found: Boolean(carrera), carreraId });
|
|
||||||
if (carreraErr) warn("Carrera fetch error (continuing)", { message: carreraErr.message });
|
|
||||||
|
|
||||||
// 5) Resolver estructura (schema) desde estructuras_plan.definicion 🧩
|
|
||||||
log("Step 5: Resolve estructura_plan 🧩");
|
|
||||||
const estructuraIdFromPayload = datosBasicos?.estructuraId ?? payload?.estructuraId ?? null;
|
|
||||||
|
|
||||||
let estructura: any = null;
|
|
||||||
const tEst = performance.now();
|
|
||||||
|
|
||||||
if (estructuraIdFromPayload) {
|
|
||||||
log("Using estructuraId from payload", { estructuraIdFromPayload });
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("estructuras_plan")
|
|
||||||
.select("id,nombre,tipo,template,definicion")
|
|
||||||
.eq("id", estructuraIdFromPayload)
|
|
||||||
.single();
|
|
||||||
if (error) {
|
|
||||||
errlog("estructuraId provided but not found", { message: error.message, estructuraIdFromPayload });
|
|
||||||
return json({ ok: false, error: "estructuraId not found", details: error.message, cid }, 400);
|
|
||||||
}
|
|
||||||
estructura = data;
|
|
||||||
} else {
|
|
||||||
// default: la más reciente tipo CURRICULAR, si no existe, la más reciente en general
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("estructuras_plan")
|
|
||||||
.select("id,nombre,tipo,template,definicion")
|
|
||||||
.eq("tipo", "CURRICULAR")
|
|
||||||
.order("actualizado_en", { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
warn("Error fetching CURRICULAR estructura (fallback to any)", { message: error.message });
|
|
||||||
}
|
|
||||||
if (data) estructura = data;
|
|
||||||
|
|
||||||
if (!estructura) {
|
|
||||||
const { data: anyE, error: anyErr } = await supabaseAdmin
|
|
||||||
.from("estructuras_plan")
|
|
||||||
.select("id,nombre,tipo,template,definicion")
|
|
||||||
.order("actualizado_en", { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle();
|
|
||||||
if (anyErr) {
|
|
||||||
errlog("No estructuras_plan found", { message: anyErr.message });
|
|
||||||
return json({ ok: false, error: "No estructuras_plan found", details: anyErr.message, cid }, 500);
|
|
||||||
}
|
|
||||||
estructura = anyE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Estructura resolved ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tEst),
|
|
||||||
estructuraId: estructura?.id ?? null,
|
|
||||||
nombre: estructura?.nombre ?? null,
|
|
||||||
tipo: estructura?.tipo ?? null,
|
|
||||||
template: estructura?.template ?? null,
|
|
||||||
definicionKeys: estructura?.definicion ? Object.keys(estructura.definicion) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!estructura?.definicion) {
|
|
||||||
errlog("estructuras_plan.definicion missing", { estructuraId: estructura?.id ?? null });
|
|
||||||
return json({ ok: false, error: "estructuras_plan.definicion missing", cid }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6) Resolver referencias (archivos / repositorios) 📎
|
|
||||||
log("Step 6: Resolve references 📎");
|
|
||||||
const uploadedToStorage: string[] = Array.isArray(iaConfig?.uploadedToStorage)
|
|
||||||
? iaConfig.uploadedToStorage
|
|
||||||
: [];
|
|
||||||
const repositoriosIds: string[] = Array.isArray(iaConfig?.repositoriosIds)
|
|
||||||
? iaConfig.repositoriosIds
|
|
||||||
: [];
|
|
||||||
|
|
||||||
log("Reference IDs received", {
|
|
||||||
uploadedToStorageCount: uploadedToStorage.length,
|
|
||||||
repositoriosIdsCount: repositoriosIds.length,
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
});
|
|
||||||
|
|
||||||
const archivosInfo: any[] = [];
|
|
||||||
const vectorInfo: any[] = [];
|
|
||||||
|
|
||||||
if (uploadedToStorage.length) {
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("archivos")
|
|
||||||
.select("id,nombre,mime_type,openai_file_id,ruta_storage")
|
|
||||||
.in("id", uploadedToStorage);
|
|
||||||
if (error) warn("archivos lookup failed (continuing)", { message: error.message });
|
|
||||||
for (const a of data ?? []) archivosInfo.push(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repositoriosIds.length) {
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("vector_stores")
|
|
||||||
.select("id,nombre,openai_vector_id")
|
|
||||||
.in("id", repositoriosIds);
|
|
||||||
if (error) warn("vector_stores lookup failed (continuing)", { message: error.message });
|
|
||||||
for (const v of data ?? []) vectorInfo.push(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiFileIds: string[] = archivosInfo
|
|
||||||
.map((a) => a?.openai_file_id)
|
|
||||||
.filter((x) => typeof x === "string" && x.length > 0);
|
|
||||||
const vectorStoreIds: string[] = vectorInfo
|
|
||||||
.map((v) => v?.openai_vector_id)
|
|
||||||
.filter((x) => typeof x === "string" && x.length > 0);
|
|
||||||
|
|
||||||
const rutasStorage: string[] = archivosInfo
|
|
||||||
.map((a) => a?.ruta_storage)
|
|
||||||
.filter((x) => typeof x === "string" && x.length > 0);
|
|
||||||
|
|
||||||
log("Resolved reference OpenAI IDs", {
|
|
||||||
openaiFileIdsCount: openaiFileIds.length,
|
|
||||||
vectorStoreIdsCount: vectorStoreIds.length,
|
|
||||||
rutasStorageCount: rutasStorage.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7) Construye prompt + structured wrapper 🧠
|
|
||||||
log("Step 7: Build prompt + structured schema 🧠");
|
|
||||||
const { system, user: userPrompt, computed } = buildPrompt({
|
|
||||||
datosBasicos,
|
|
||||||
carrera,
|
|
||||||
estructura,
|
|
||||||
iaConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
// definicion en DB puede venir como (schema) o como (format). Normalizamos.
|
|
||||||
let structured: any = null;
|
|
||||||
const definicion = estructura.definicion;
|
|
||||||
|
|
||||||
if (definicion?.type === "json_schema" && definicion?.schema) {
|
|
||||||
structured = definicion;
|
|
||||||
} else if (definicion?.format?.type === "json_schema" && definicion?.format?.schema) {
|
|
||||||
structured = definicion;
|
|
||||||
} else {
|
|
||||||
structured = {
|
|
||||||
type: "json_schema",
|
|
||||||
name: "plan_datos",
|
|
||||||
strict: true,
|
|
||||||
schema: definicion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Structured wrapper summary", {
|
|
||||||
type: structured?.type ?? null,
|
|
||||||
name: structured?.name ?? null,
|
|
||||||
strict: structured?.strict ?? null,
|
|
||||||
hasSchema: Boolean(structured?.schema),
|
|
||||||
schemaTopKeys: structured?.schema ? Object.keys(structured.schema).slice(0, 20) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8) Llamar ai-structured (otra Edge Function) 🔁
|
|
||||||
log("Step 8: Call ai-structured 🔁");
|
|
||||||
const aiStructuredUrl = `${SUPABASE_URL}/functions/v1/ai-structured`;
|
|
||||||
|
|
||||||
const model = payload?.model ?? payload?.openai?.model ?? payload?.response?.model ?? "gpt-5";
|
|
||||||
|
|
||||||
const aiStructuredBody = {
|
|
||||||
response: {
|
|
||||||
model,
|
|
||||||
input: [
|
|
||||||
{ role: "system", content: system },
|
|
||||||
{ role: "user", content: userPrompt },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
structured,
|
|
||||||
references: {
|
|
||||||
openaiFileIds,
|
|
||||||
vectorStoreIds,
|
|
||||||
uploadedToStorage,
|
|
||||||
},
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
};
|
|
||||||
|
|
||||||
log("ai-structured request snapshot 🧪", {
|
|
||||||
url: aiStructuredUrl,
|
|
||||||
model,
|
|
||||||
inputChars: { system: system.length, user: userPrompt.length },
|
|
||||||
openaiFileIdsCount: openaiFileIds.length,
|
|
||||||
vectorStoreIdsCount: vectorStoreIds.length,
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tAi = performance.now();
|
|
||||||
const aiResp = await fetch(aiStructuredUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: authHeaderRaw,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(aiStructuredBody),
|
|
||||||
});
|
|
||||||
|
|
||||||
const aiJson = await aiResp.json().catch(() => null);
|
|
||||||
|
|
||||||
log("ai-structured response received ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tAi),
|
|
||||||
status: aiResp.status,
|
|
||||||
okHttp: aiResp.ok,
|
|
||||||
ok: aiJson?.ok ?? null,
|
|
||||||
aiCid: aiJson?.cid ?? null,
|
|
||||||
responseId: aiJson?.responseId ?? null,
|
|
||||||
conversationId: aiJson?.conversationId ?? null,
|
|
||||||
outputParseError: aiJson?.outputParseError ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!aiResp.ok || !aiJson) {
|
|
||||||
errlog("ai-structured HTTP failed", { status: aiResp.status, body: aiJson });
|
|
||||||
return json({
|
|
||||||
ok: false,
|
|
||||||
error: "ai-structured HTTP failed",
|
|
||||||
status: aiResp.status,
|
|
||||||
details: aiJson,
|
|
||||||
cid,
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!aiJson.ok || !aiJson.output) {
|
|
||||||
errlog("ai-structured returned ok=false or missing output", { aiJson });
|
|
||||||
// Aun así, registra interaccion_ia (sin plan) para auditoría
|
|
||||||
const { data: inter, error: interErr } = await supabaseAdmin
|
|
||||||
.from("interacciones_ia")
|
|
||||||
.insert({
|
|
||||||
usuario_id: user.id,
|
|
||||||
plan_estudio_id: null,
|
|
||||||
tipo: "GENERAR",
|
|
||||||
modelo: aiJson?.model ?? model,
|
|
||||||
temperatura: null,
|
|
||||||
prompt: {
|
|
||||||
datosBasicos,
|
|
||||||
iaConfig,
|
|
||||||
estructura: { id: estructura.id, nombre: estructura.nombre, template: estructura.template, tipo: estructura.tipo },
|
|
||||||
request: aiStructuredBody,
|
|
||||||
computed,
|
|
||||||
},
|
|
||||||
respuesta: aiJson,
|
|
||||||
aceptada: false,
|
|
||||||
conversacion_id: aiJson?.conversationId ?? null,
|
|
||||||
ids_archivos: uploadedToStorage,
|
|
||||||
ids_vector_store: repositoriosIds,
|
|
||||||
rutas_storage: rutasStorage,
|
|
||||||
})
|
|
||||||
.select("id")
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (interErr) warn("interacciones_ia insert failed (error path)", { message: interErr.message });
|
|
||||||
|
|
||||||
return json({
|
|
||||||
ok: false,
|
|
||||||
error: "AI generation failed (structured output invalid).",
|
|
||||||
cid,
|
|
||||||
ai: aiJson,
|
|
||||||
interaccion_ia_id: inter?.id ?? null,
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9) Normaliza/asegura campos clave del output ✅
|
|
||||||
log("Step 9: Normalize output ✅");
|
|
||||||
const output = aiJson.output as Record<string, any>;
|
|
||||||
|
|
||||||
// fuerza consistencia con datosBasicos
|
|
||||||
output.nombre = computed.nombrePlan;
|
|
||||||
output.nivel = computed.nivel;
|
|
||||||
output.total_de_ciclos_del_plan_de_estudios = String(computed.numCiclos);
|
|
||||||
output.duracion_del_ciclo_escolar = String(computed.semanas);
|
|
||||||
|
|
||||||
// 10) Inserta log de IA (interacciones_ia) 🧾
|
|
||||||
log("Step 10: Insert interacciones_ia 🧾");
|
|
||||||
const tInter = performance.now();
|
|
||||||
const { data: interaccion, error: interErr } = await supabaseAdmin
|
|
||||||
.from("interacciones_ia")
|
|
||||||
.insert({
|
|
||||||
usuario_id: user.id,
|
|
||||||
plan_estudio_id: null, // se linkea luego (cuando exista el plan)
|
|
||||||
tipo: "GENERAR",
|
|
||||||
modelo: aiJson?.model ?? model,
|
|
||||||
temperatura: null,
|
|
||||||
prompt: {
|
|
||||||
datosBasicos,
|
|
||||||
iaConfig,
|
|
||||||
estructura: {
|
|
||||||
id: estructura.id,
|
|
||||||
nombre: estructura.nombre,
|
|
||||||
template: estructura.template,
|
|
||||||
tipo: estructura.tipo,
|
|
||||||
},
|
|
||||||
request: aiStructuredBody,
|
|
||||||
computed,
|
|
||||||
},
|
|
||||||
respuesta: aiJson,
|
|
||||||
aceptada: true, // se está usando para crear el plan
|
|
||||||
conversacion_id: aiJson?.conversationId ?? null,
|
|
||||||
ids_archivos: uploadedToStorage,
|
|
||||||
ids_vector_store: repositoriosIds,
|
|
||||||
rutas_storage: rutasStorage,
|
|
||||||
})
|
|
||||||
.select("id")
|
|
||||||
.single();
|
|
||||||
|
|
||||||
log("interacciones_ia insert done ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tInter),
|
|
||||||
interaccionId: interaccion?.id ?? null,
|
|
||||||
ok: !interErr,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (interErr) {
|
|
||||||
errlog("interacciones_ia insert failed", { message: interErr.message });
|
|
||||||
return json(
|
|
||||||
{ ok: false, error: "interacciones_ia insert failed", details: interErr.message, cid },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11) Crear plan (planes_estudio) 🏗️
|
|
||||||
log("Step 11: Insert planes_estudio 🏗️");
|
|
||||||
const nivelDB = normalizeNivel(String(datosBasicos?.nivel ?? ""));
|
|
||||||
const tipoCicloDB = normalizeTipoCiclo(String(datosBasicos?.tipoCiclo ?? ""));
|
|
||||||
const numeroCiclosDB = Number(datosBasicos?.numCiclos ?? 0);
|
|
||||||
|
|
||||||
// intenta setear estado BORRADOR si existe
|
|
||||||
let estadoBorradorId: string | null = null;
|
|
||||||
try {
|
|
||||||
const { data: estado } = await supabaseAdmin
|
|
||||||
.from("estados_plan")
|
|
||||||
.select("id,clave,orden")
|
|
||||||
.ilike("clave", "BORRADOR%")
|
|
||||||
.order("orden", { ascending: true })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle();
|
|
||||||
estadoBorradorId = estado?.id ?? null;
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
const tPlan = performance.now();
|
|
||||||
const { data: plan, error: planErr } = await supabaseAdmin
|
|
||||||
.from("planes_estudio")
|
|
||||||
.insert({
|
|
||||||
carrera_id: carreraId,
|
|
||||||
estructura_id: estructura.id,
|
|
||||||
nombre: computed.nombrePlan,
|
|
||||||
nivel: nivelDB,
|
|
||||||
tipo_ciclo: tipoCicloDB,
|
|
||||||
numero_ciclos: numeroCiclosDB,
|
|
||||||
datos: output,
|
|
||||||
estado_actual_id: estadoBorradorId,
|
|
||||||
activo: true,
|
|
||||||
tipo_origen: "IA",
|
|
||||||
meta_origen: {
|
|
||||||
generado_por: "ai_generate_plan",
|
|
||||||
cid,
|
|
||||||
ai_structured: {
|
|
||||||
cid: aiJson?.cid ?? null,
|
|
||||||
responseId: aiJson?.responseId ?? null,
|
|
||||||
conversationId: aiJson?.conversationId ?? null,
|
|
||||||
model: aiJson?.model ?? model,
|
|
||||||
usage: aiJson?.usage ?? null,
|
|
||||||
},
|
|
||||||
referencias: {
|
|
||||||
uploadedToStorage,
|
|
||||||
repositoriosIds,
|
|
||||||
openaiFileIds,
|
|
||||||
vectorStoreIds,
|
|
||||||
},
|
|
||||||
iaConfig: {
|
|
||||||
descripcionEnfoque: iaConfig?.descripcionEnfoque ?? null,
|
|
||||||
poblacionObjetivo: iaConfig?.poblacionObjetivo ?? null,
|
|
||||||
notasAdicionales: iaConfig?.notasAdicionales ?? null,
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
creado_por: user.id,
|
|
||||||
actualizado_por: user.id,
|
|
||||||
})
|
|
||||||
.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();
|
|
||||||
|
|
||||||
log("planes_estudio insert done ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tPlan),
|
|
||||||
planId: plan?.id ?? null,
|
|
||||||
ok: !planErr,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (planErr) {
|
|
||||||
errlog("planes_estudio insert failed", { message: planErr.message });
|
|
||||||
return json(
|
|
||||||
{ ok: false, error: "planes_estudio insert failed", details: planErr.message, cid },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12) Link interaccion_ia -> plan + historial (cambios_plan) 🧩
|
|
||||||
log("Step 12: Link interaccion + insert cambios_plan 🧩");
|
|
||||||
|
|
||||||
const { error: linkErr } = await supabaseAdmin
|
|
||||||
.from("interacciones_ia")
|
|
||||||
.update({ plan_estudio_id: plan.id })
|
|
||||||
.eq("id", interaccion.id);
|
|
||||||
|
|
||||||
if (linkErr) warn("Could not link interaccion_ia to plan (continuing)", { message: linkErr.message });
|
|
||||||
|
|
||||||
const { error: cambioErr } = await supabaseAdmin
|
|
||||||
.from("cambios_plan")
|
|
||||||
.insert({
|
|
||||||
plan_estudio_id: plan.id,
|
|
||||||
cambiado_por: user.id,
|
|
||||||
tipo: "ACTUALIZACION_CAMPO",
|
|
||||||
campo: "datos",
|
|
||||||
valor_anterior: null,
|
|
||||||
valor_nuevo: output,
|
|
||||||
interaccion_ia_id: interaccion.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cambioErr) warn("cambios_plan insert failed (continuing)", { message: cambioErr.message });
|
|
||||||
|
|
||||||
// 13) (OPCIONAL) Generar materias automáticamente (dejado comentado) 📚
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// const AUTO_GENERATE_SUBJECTS = false; // 👈 activar cuando decidas
|
|
||||||
// const AUTO_SUBJECTS_COUNT = 10; // 👈 constante pedida (número de materias)
|
|
||||||
// if (AUTO_GENERATE_SUBJECTS) {
|
|
||||||
// for (let i = 1; i <= AUTO_SUBJECTS_COUNT; i++) {
|
|
||||||
// // TODO: insertar en `asignaturas` con plan_estudio_id = plan.id
|
|
||||||
// // Recomendación: usar una estructura_asignatura default y llenar datos mínimos.
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
log("RETURN ✅", {
|
|
||||||
ok: true,
|
|
||||||
planId: plan.id,
|
|
||||||
interaccionIaId: interaccion.id,
|
|
||||||
totalMs: Math.round(performance.now() - t0),
|
|
||||||
});
|
|
||||||
|
|
||||||
return json({
|
|
||||||
ok: true,
|
|
||||||
cid,
|
|
||||||
plan,
|
|
||||||
plan_datos: output,
|
|
||||||
interaccion_ia_id: interaccion.id,
|
|
||||||
ai: {
|
|
||||||
cid: aiJson?.cid ?? null,
|
|
||||||
responseId: aiJson?.responseId ?? null,
|
|
||||||
conversationId: aiJson?.conversationId ?? null,
|
|
||||||
model: aiJson?.model ?? model,
|
|
||||||
usage: aiJson?.usage ?? null,
|
|
||||||
},
|
|
||||||
references: {
|
|
||||||
uploadedToStorage,
|
|
||||||
repositoriosIds,
|
|
||||||
openaiFileIds,
|
|
||||||
vectorStoreIds,
|
|
||||||
rutasStorage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
errlog("Unhandled error 💥", {
|
|
||||||
message: (e as Error)?.message ?? String(e),
|
|
||||||
stack: (e as Error)?.stack ?? null,
|
|
||||||
totalMs: Math.round(performance.now() - t0),
|
|
||||||
});
|
|
||||||
return json({ ok: false, error: (e as Error)?.message ?? String(e), cid }, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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,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,331 @@
|
|||||||
|
const CARBONE_VERSION = "5";
|
||||||
|
|
||||||
|
export type CarboneSuccess<T> = {
|
||||||
|
success: true;
|
||||||
|
data: T;
|
||||||
|
hasMore?: boolean;
|
||||||
|
nextCursor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CarboneError = {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CarboneResponse<T> = CarboneSuccess<T> | CarboneError;
|
||||||
|
|
||||||
|
export type CarboneDownloadResult = {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
contentType: string | null;
|
||||||
|
contentDisposition: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CarboneClient {
|
||||||
|
constructor(
|
||||||
|
private baseUrl: string,
|
||||||
|
private apiToken: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private buildHeaders(extra?: Record<string, string>) {
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${this.apiToken}`,
|
||||||
|
"carbone-version": CARBONE_VERSION,
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseResponse<T>(res: Response): Promise<T> {
|
||||||
|
const contentType = res.headers.get("content-type") || "";
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = `Carbone request failed: ${res.status}`;
|
||||||
|
try {
|
||||||
|
const err = await res.json();
|
||||||
|
message = err?.error || err?.message || message;
|
||||||
|
} catch {
|
||||||
|
// ignore json parse errors
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType.includes("application/json")) {
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res as unknown as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toQuery(params?: Record<string, unknown>) {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (!params) return "";
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value === undefined || value === null) continue;
|
||||||
|
qs.set(key, String(value));
|
||||||
|
}
|
||||||
|
const s = qs.toString();
|
||||||
|
return s ? `?${s}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTemplates(params?: {
|
||||||
|
id?: string;
|
||||||
|
versionId?: string;
|
||||||
|
category?: string;
|
||||||
|
origin?: 0 | 1;
|
||||||
|
includeVersions?: boolean;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}) {
|
||||||
|
const res = await fetch(
|
||||||
|
`${this.baseUrl}/templates${this.toQuery(params)}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return this.parseResponse<
|
||||||
|
CarboneResponse<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
versionId: string;
|
||||||
|
deployedAt: number;
|
||||||
|
createdAt: number;
|
||||||
|
expireAt?: number;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
name?: string;
|
||||||
|
category?: string;
|
||||||
|
comment?: string;
|
||||||
|
tags?: string[];
|
||||||
|
origin?: number;
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listCategories() {
|
||||||
|
const res = await fetch(`${this.baseUrl}/templates/categories`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.buildHeaders(),
|
||||||
|
});
|
||||||
|
return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTags() {
|
||||||
|
const res = await fetch(`${this.baseUrl}/templates/tags`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.buildHeaders(),
|
||||||
|
});
|
||||||
|
return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadTemplateJson(body: {
|
||||||
|
template: string;
|
||||||
|
versioning?: boolean;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
comment?: string;
|
||||||
|
tags?: string[];
|
||||||
|
category?: string;
|
||||||
|
sample?: unknown[];
|
||||||
|
deployedAt?: number;
|
||||||
|
expireAt?: number;
|
||||||
|
}) {
|
||||||
|
const res = await fetch(`${this.baseUrl}/template`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.buildHeaders({
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
return this.parseResponse<
|
||||||
|
CarboneResponse<{
|
||||||
|
id?: string;
|
||||||
|
versionId?: string;
|
||||||
|
templateId?: string;
|
||||||
|
type?: string;
|
||||||
|
size?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
}>
|
||||||
|
>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadTemplateFile(input: {
|
||||||
|
file: Blob;
|
||||||
|
filename: string;
|
||||||
|
versioning?: boolean;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
comment?: string;
|
||||||
|
tags?: string[];
|
||||||
|
category?: string;
|
||||||
|
sample?: unknown[];
|
||||||
|
deployedAt?: number;
|
||||||
|
expireAt?: number;
|
||||||
|
}) {
|
||||||
|
const form = new FormData();
|
||||||
|
|
||||||
|
if (input.versioning !== undefined) {
|
||||||
|
form.append("versioning", String(input.versioning));
|
||||||
|
}
|
||||||
|
if (input.id) form.append("id", input.id);
|
||||||
|
if (input.name) form.append("name", input.name);
|
||||||
|
if (input.comment) form.append("comment", input.comment);
|
||||||
|
if (input.category) form.append("category", input.category);
|
||||||
|
if (input.tags) {
|
||||||
|
for (const tag of input.tags) form.append("tags[]", tag);
|
||||||
|
}
|
||||||
|
if (input.sample) form.append("sample", JSON.stringify(input.sample));
|
||||||
|
if (input.deployedAt !== undefined) {
|
||||||
|
form.append("deployedAt", String(input.deployedAt));
|
||||||
|
}
|
||||||
|
if (input.expireAt !== undefined) {
|
||||||
|
form.append("expireAt", String(input.expireAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// template should be appended last
|
||||||
|
form.append("template", input.file, input.filename);
|
||||||
|
|
||||||
|
const res = await fetch(`${this.baseUrl}/template`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.buildHeaders(),
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
return this.parseResponse<
|
||||||
|
CarboneResponse<{
|
||||||
|
id?: string;
|
||||||
|
versionId?: string;
|
||||||
|
templateId?: string;
|
||||||
|
type?: string;
|
||||||
|
size?: number;
|
||||||
|
createdAt?: number;
|
||||||
|
}>
|
||||||
|
>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async patchTemplate(
|
||||||
|
id: string,
|
||||||
|
body: {
|
||||||
|
name?: string;
|
||||||
|
comment?: string;
|
||||||
|
tags?: string[];
|
||||||
|
category?: string;
|
||||||
|
deployedAt?: number;
|
||||||
|
expireAt?: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const res = await fetch(`${this.baseUrl}/template/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: this.buildHeaders({
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
return this.parseResponse<CarboneResponse<true>>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTemplate(id: string) {
|
||||||
|
const res = await fetch(`${this.baseUrl}/template/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: this.buildHeaders(),
|
||||||
|
});
|
||||||
|
return this.parseResponse<CarboneResponse<true>>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadTemplate(id: string): Promise<CarboneDownloadResult> {
|
||||||
|
const res = await fetch(`${this.baseUrl}/template/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.buildHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(
|
||||||
|
err?.error || `Failed to download template: ${res.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: new Uint8Array(await res.arrayBuffer()),
|
||||||
|
contentType: res.headers.get("content-type"),
|
||||||
|
contentDisposition: res.headers.get("content-disposition"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async render(
|
||||||
|
templateIdOrVersionId: string,
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
opts?: { download?: boolean },
|
||||||
|
) {
|
||||||
|
const query = opts?.download ? "?download=true" : "";
|
||||||
|
const res = await fetch(
|
||||||
|
`${this.baseUrl}/render/${templateIdOrVersionId}${query}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: this.buildHeaders({
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opts?.download) {
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(err?.error || `Render failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
buffer: new Uint8Array(await res.arrayBuffer()),
|
||||||
|
contentType: res.headers.get("content-type"),
|
||||||
|
contentDisposition: res.headers.get("content-disposition"),
|
||||||
|
} satisfies CarboneDownloadResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderFromTemplate(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
opts?: { download?: boolean },
|
||||||
|
) {
|
||||||
|
const query = opts?.download ? "?download=true" : "";
|
||||||
|
const res = await fetch(`${this.baseUrl}/render/template${query}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.buildHeaders({
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts?.download) {
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(err?.error || `Render failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
buffer: new Uint8Array(await res.arrayBuffer()),
|
||||||
|
contentType: res.headers.get("content-type"),
|
||||||
|
contentDisposition: res.headers.get("content-disposition"),
|
||||||
|
} satisfies CarboneDownloadResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadRender(renderId: string): Promise<CarboneDownloadResult> {
|
||||||
|
const res = await fetch(`${this.baseUrl}/render/${renderId}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error(err?.error || `Failed to download render: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: new Uint8Array(await res.arrayBuffer()),
|
||||||
|
contentType: res.headers.get("content-type"),
|
||||||
|
contentDisposition: res.headers.get("content-disposition"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||||
|
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
||||||
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts",
|
||||||
|
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { corsHeaders } from "../_shared/cors.ts";
|
||||||
|
import type { Database, Json } from "../_shared/database.types.ts";
|
||||||
|
import { HttpError } from "../_shared/utils.ts";
|
||||||
|
import { CarboneClient } from "./carbone.ts";
|
||||||
|
|
||||||
|
const DownloadReportBodySchema = z.record(z.unknown()).optional().default({});
|
||||||
|
|
||||||
|
const DownloadReportPlanSchema = z
|
||||||
|
.object({
|
||||||
|
action: z.literal("downloadReport"),
|
||||||
|
plan_estudio_id: z.string().min(1),
|
||||||
|
body: DownloadReportBodySchema,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const DownloadReportAsignaturaSchema = z
|
||||||
|
.object({
|
||||||
|
action: z.literal("downloadReport"),
|
||||||
|
asignatura_id: z.string().min(1),
|
||||||
|
body: DownloadReportBodySchema,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const DownloadReportSchema = z.union([
|
||||||
|
DownloadReportPlanSchema,
|
||||||
|
DownloadReportAsignaturaSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type DownloadReportInput = z.infer<typeof DownloadReportSchema>;
|
||||||
|
|
||||||
|
type SupabaseClient = ReturnType<typeof createClient<Database>>;
|
||||||
|
|
||||||
|
type CarboneDownload = {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
contentType: string | null;
|
||||||
|
contentDisposition: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadPlanDatos(
|
||||||
|
supabase: SupabaseClient,
|
||||||
|
planEstudioId: string,
|
||||||
|
): Promise<{ datos: Json; estructura_id: string }> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("planes_estudio")
|
||||||
|
.select("datos, estructura_id")
|
||||||
|
.eq("id", planEstudioId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Error consultando el plan de estudios.",
|
||||||
|
"DB_ERROR",
|
||||||
|
{
|
||||||
|
message: error.message,
|
||||||
|
details: error.details,
|
||||||
|
hint: error.hint,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
throw new HttpError(404, "Plan de estudios no encontrado.", "NOT_FOUND", {
|
||||||
|
plan_estudio_id: planEstudioId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { datos: data.datos, estructura_id: data.estructura_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTemplateIdForEstructura(
|
||||||
|
supabase: SupabaseClient,
|
||||||
|
estructuraId: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("estructuras_plan")
|
||||||
|
.select("template_id")
|
||||||
|
.eq("id", estructuraId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Error consultando la estructura del plan.",
|
||||||
|
"DB_ERROR",
|
||||||
|
{
|
||||||
|
message: error.message,
|
||||||
|
details: error.details,
|
||||||
|
hint: error.hint,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
throw new HttpError(
|
||||||
|
404,
|
||||||
|
"Estructura del plan no encontrada.",
|
||||||
|
"NOT_FOUND",
|
||||||
|
{
|
||||||
|
estructura_id: estructuraId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!data.template_id) {
|
||||||
|
throw new HttpError(
|
||||||
|
409,
|
||||||
|
"La estructura del plan no tiene template_id configurado.",
|
||||||
|
"MISSING_TEMPLATE_ID",
|
||||||
|
{ estructura_id: estructuraId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.template_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTemplateIdForAsignatura(
|
||||||
|
supabase: SupabaseClient,
|
||||||
|
asignaturaId: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("plantilla_asignatura")
|
||||||
|
.select("template_id")
|
||||||
|
.eq("asignatura_id", asignaturaId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Error consultando la plantilla de la asignatura.",
|
||||||
|
"DB_ERROR",
|
||||||
|
{
|
||||||
|
message: error.message,
|
||||||
|
details: error.details,
|
||||||
|
hint: error.hint,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
throw new HttpError(404, "Asignatura no encontrada.", "NOT_FOUND", {
|
||||||
|
asignatura_id: asignaturaId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.template_id) {
|
||||||
|
throw new HttpError(
|
||||||
|
409,
|
||||||
|
"La asignatura no tiene template_id configurado.",
|
||||||
|
"MISSING_TEMPLATE_ID",
|
||||||
|
{ asignatura_id: asignaturaId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.template_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCarboneDownload(result: unknown): CarboneDownload {
|
||||||
|
if (!(result && typeof result === "object" && "buffer" in result)) {
|
||||||
|
throw new HttpError(
|
||||||
|
502,
|
||||||
|
"Respuesta inválida de Carbone.",
|
||||||
|
"UPSTREAM_INVALID_RESPONSE",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = result as CarboneDownload;
|
||||||
|
if (!(download.buffer instanceof Uint8Array)) {
|
||||||
|
throw new HttpError(
|
||||||
|
502,
|
||||||
|
"Respuesta inválida de Carbone.",
|
||||||
|
"UPSTREAM_INVALID_RESPONSE",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return download;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadToResponse(download: CarboneDownload): Response {
|
||||||
|
const headers = new Headers({
|
||||||
|
...corsHeaders,
|
||||||
|
"Content-Type": download.contentType === "application/pdf"
|
||||||
|
? "application/pdf"
|
||||||
|
: "application/octet-stream",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (download.contentDisposition) {
|
||||||
|
headers.set("Content-Disposition", download.contentDisposition);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = new Uint8Array(
|
||||||
|
download.buffer.buffer as ArrayBuffer,
|
||||||
|
download.buffer.byteOffset,
|
||||||
|
download.buffer.byteLength,
|
||||||
|
);
|
||||||
|
return new Response(body, { status: 200, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDownloadReportAction(args: {
|
||||||
|
bodyUnknown: unknown;
|
||||||
|
supabase: SupabaseClient;
|
||||||
|
carboneBaseUrl: string;
|
||||||
|
carboneApiToken: string;
|
||||||
|
}): Promise<Response> {
|
||||||
|
const input: DownloadReportInput = DownloadReportSchema.parse(
|
||||||
|
args.bodyUnknown,
|
||||||
|
);
|
||||||
|
const carbone = new CarboneClient(args.carboneBaseUrl, args.carboneApiToken);
|
||||||
|
|
||||||
|
if ("plan_estudio_id" in input) {
|
||||||
|
const { datos, estructura_id } = await loadPlanDatos(
|
||||||
|
args.supabase,
|
||||||
|
input.plan_estudio_id,
|
||||||
|
);
|
||||||
|
const templateId = await loadTemplateIdForEstructura(
|
||||||
|
args.supabase,
|
||||||
|
estructura_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: _ignoredData, ...extraBody } = input.body;
|
||||||
|
const result = await carbone.render(
|
||||||
|
templateId,
|
||||||
|
{ data: datos, ...extraBody },
|
||||||
|
{ download: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
return downloadToResponse(ensureCarboneDownload(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateId = await loadTemplateIdForAsignatura(
|
||||||
|
args.supabase,
|
||||||
|
input.asignatura_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasData = Object.prototype.hasOwnProperty.call(input.body, "data");
|
||||||
|
if (!hasData) {
|
||||||
|
throw new HttpError(
|
||||||
|
400,
|
||||||
|
"Para asignatura_id se requiere body.data.",
|
||||||
|
"MISSING_DATA",
|
||||||
|
{ asignatura_id: input.asignatura_id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("body:", input.body);
|
||||||
|
const { data, ...extraBody } = input.body as Record<string, unknown>;
|
||||||
|
const result = await carbone.render(
|
||||||
|
templateId,
|
||||||
|
{ data, ...extraBody },
|
||||||
|
{ download: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
return downloadToResponse(ensureCarboneDownload(result));
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// 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 { createClient } from "@supabase/supabase-js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { corsHeaders } from "../_shared/cors.ts";
|
||||||
|
import type { Database } from "../_shared/database.types.ts";
|
||||||
|
import { HttpError, sendError } from "../_shared/utils.ts";
|
||||||
|
import { handleDownloadReportAction } from "./download-report.ts";
|
||||||
|
|
||||||
|
const ActionSchema = z.object({
|
||||||
|
action: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAuthHeader(req: Request): string | null {
|
||||||
|
return req.headers.get("Authorization") ?? req.headers.get("authorization");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 authHeader = getAuthHeader(req);
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new HttpError(401, "No autorizado.", "UNAUTHORIZED", {
|
||||||
|
reason: "missing_authorization_header",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = (req.headers.get("content-type") || "").toLowerCase();
|
||||||
|
if (!contentType.startsWith("application/json")) {
|
||||||
|
throw new HttpError(
|
||||||
|
415,
|
||||||
|
"Content-Type no soportado.",
|
||||||
|
"UNSUPPORTED_MEDIA_TYPE",
|
||||||
|
{ contentType },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
||||||
|
const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
||||||
|
if (!SUPABASE_URL || !SERVICE_ROLE_KEY) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Configuración del servidor incompleta.",
|
||||||
|
"MISSING_ENV",
|
||||||
|
{
|
||||||
|
missing: [
|
||||||
|
!SUPABASE_URL ? "SUPABASE_URL" : null,
|
||||||
|
!SERVICE_ROLE_KEY ? "SUPABASE_SERVICE_ROLE_KEY" : null,
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CARBONE_API_TOKEN = Deno.env.get("CARBONE_API_TOKEN");
|
||||||
|
const CARBONE_BASE_URL = Deno.env.get("CARBONE_BASE_URL") ||
|
||||||
|
"https://carbone.lci.ulsa.mx";
|
||||||
|
if (!CARBONE_API_TOKEN) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Configuración del servidor incompleta.",
|
||||||
|
"MISSING_ENV",
|
||||||
|
{
|
||||||
|
missing: [
|
||||||
|
!CARBONE_API_TOKEN ? "CARBONE_API_TOKEN" : null,
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyUnknown: unknown = await req.json();
|
||||||
|
const { action } = ActionSchema.parse(bodyUnknown);
|
||||||
|
|
||||||
|
const supabase = createClient<Database>(SUPABASE_URL, SERVICE_ROLE_KEY, {
|
||||||
|
auth: { persistSession: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "downloadReport": {
|
||||||
|
const response = await handleDownloadReportAction({
|
||||||
|
bodyUnknown,
|
||||||
|
supabase,
|
||||||
|
carboneBaseUrl: CARBONE_BASE_URL,
|
||||||
|
carboneApiToken: CARBONE_API_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${
|
||||||
|
new Date().toISOString()
|
||||||
|
}][${functionName}]: Request processed successfully`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new HttpError(
|
||||||
|
400,
|
||||||
|
"Acción no soportada.",
|
||||||
|
"UNSUPPORTED_ACTION",
|
||||||
|
{ action },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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:
|
||||||
|
|
||||||
|
# Requires secrets:
|
||||||
|
# - CARBONE_API_TOKEN
|
||||||
|
# Optional:
|
||||||
|
# - CARBONE_BASE_URL (defaults to https://carbone.lci.ulsa.mx)
|
||||||
|
|
||||||
|
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/carbone-io-wrapper' \
|
||||||
|
--header 'Authorization: Bearer <JWT>' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{"action":"downloadReport","plan_estudio_id":"<uuid>","body":{}}'
|
||||||
|
|
||||||
|
# Or for asignaturas (must include body.data):
|
||||||
|
|
||||||
|
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/carbone-io-wrapper' \
|
||||||
|
--header 'Authorization: Bearer <JWT>' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{"action":"downloadReport","asignatura_id":"<uuid>","body":{"data":{}}}'
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,123 +1,414 @@
|
|||||||
// Setup type definitions for built-in Supabase Runtime APIs
|
import { Hono } from "jsr:@hono/hono";
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import { corsHeaders, withCors } from "./lib/cors.ts";
|
||||||
import { corsHeaders } from "../_shared/cors.ts";
|
import { HttpError, jsonResponse } from "./lib/errors.ts";
|
||||||
|
import { getOpenAI } from "./lib/openai.ts";
|
||||||
|
import { getSupabaseServiceClient, requireUser } from "./lib/supabase.ts";
|
||||||
|
import { assertUuid, pickSchemaFields, safePlanForPrompt,pickSchemaAsignaturaFields, safeAsignaturaForPrompt, getAsignaturaSystemPrompt } from "./lib/plan.ts";
|
||||||
import { OpenAIService } from "../_shared/openai-service.ts";
|
import { OpenAIService } from "../_shared/openai-service.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
||||||
|
|
||||||
type WebhookPayload = {
|
type CreateBody = {
|
||||||
type: "INSERT" | "UPDATE" | "DELETE";
|
plan_estudio_id: string;
|
||||||
table: string;
|
instanciador?: string;
|
||||||
schema: string;
|
system_prompt?: string;
|
||||||
record: Record<string, unknown> | null;
|
|
||||||
old_record: Record<string, unknown> | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
|
type AddMessageBody = {
|
||||||
const SUPABASE_SERVICE_ROLE_KEY =
|
// Guarda mensaje en OpenAI conversation
|
||||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
content: string;
|
||||||
|
// Si quieres forzar mejoras estructuradas:
|
||||||
|
campos?: string[];
|
||||||
|
user_prompt?: string; // si no mandas, usa content
|
||||||
|
model?: string; // default gpt-5-nano
|
||||||
|
};
|
||||||
|
|
||||||
const ALLOWED_SCHEMA = "public";
|
const app = new Hono();
|
||||||
const ALLOWED_TABLES = new Set(["planes_estudio", "asignaturas"]);
|
|
||||||
|
|
||||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
addEventListener("beforeunload", (ev: any) => {
|
||||||
auth: { persistSession: false },
|
console.error(
|
||||||
|
"ALERTA: La función se va a apagar. Razón:",
|
||||||
|
ev?.detail?.reason,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function jsonResponse(status: number, body: Record<string, unknown>) {
|
// Preflight CORS
|
||||||
return new Response(JSON.stringify(body), {
|
app.options(
|
||||||
status,
|
"*",
|
||||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
(c) => new Response(null, { status: 204, headers: corsHeaders }),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
async function createConversationId(
|
const prefix = "/create-chat-conversation";
|
||||||
openaiService: OpenAIService,
|
// Model names (module-level) — pueden ser sobrescritos por variables de entorno
|
||||||
metadata: Record<string, string>,
|
const CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO = Deno.env.get(
|
||||||
) {
|
"CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO",
|
||||||
const conversation = await openaiService.createConversation(metadata);
|
) ?? "gpt-5-nano";
|
||||||
const conversationId = conversation?.id as string | undefined;
|
const CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO = Deno.env.get(
|
||||||
|
"CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO",
|
||||||
|
) ?? "gpt-5-nano";
|
||||||
|
|
||||||
if (!conversationId) {
|
app.get(`${prefix}/health`, (c) => withCors(jsonResponse({ ok: true })));
|
||||||
throw new Error("OpenAI response missing conversation id");
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversationId;
|
/**
|
||||||
}
|
* POST /conversations
|
||||||
|
* Crea conversación OpenAI + registro en conversaciones_plan
|
||||||
Deno.serve(async (req) => {
|
*/
|
||||||
if (req.method !== "POST") {
|
app.post(`${prefix}/plan/conversations`, async (c) => {
|
||||||
return jsonResponse(405, { error: "Method not allowed" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
|
||||||
return jsonResponse(500, { error: "Supabase env vars missing" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiService = OpenAIService.fromEnv();
|
|
||||||
if (!(openaiService instanceof OpenAIService)) {
|
|
||||||
return jsonResponse(500, { error: openaiService.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload: WebhookPayload;
|
|
||||||
try {
|
try {
|
||||||
payload = await req.json();
|
/* const auth = c.req.header("authorization");
|
||||||
} catch {
|
const user = await requireUser(auth); */
|
||||||
return jsonResponse(400, { error: "Invalid JSON body" });
|
|
||||||
|
const body = (await c.req.json().catch(() => ({}))) as Partial<CreateBody>;
|
||||||
|
const plan_estudio_id = body.plan_estudio_id;
|
||||||
|
assertUuid(plan_estudio_id ?? "", "plan_estudio_id");
|
||||||
|
|
||||||
|
const instanciador = /* user.email ?? user.id ?? */ body.instanciador ??
|
||||||
|
"unknown";
|
||||||
|
const system_prompt = body.system_prompt ??
|
||||||
|
"En caso de que te pidan algo que no tiene nada que ver con planes de estudio o asignatura responde con un refusal.";
|
||||||
|
|
||||||
|
const supabase = getSupabaseServiceClient();
|
||||||
|
const openai = getOpenAI();
|
||||||
|
|
||||||
|
// Cargar plan + estructura
|
||||||
|
const { data: plan, error: planErr } = await supabase
|
||||||
|
.from("planes_estudio")
|
||||||
|
.select("*, estructuras_plan (definicion)")
|
||||||
|
.eq("id", plan_estudio_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (planErr || !plan) {
|
||||||
|
throw new HttpError(
|
||||||
|
404,
|
||||||
|
"plan_not_found",
|
||||||
|
"Plan de estudio no encontrado",
|
||||||
|
planErr,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, table, schema, record } = payload;
|
// Crear conversación en OpenAI
|
||||||
|
const conv = await openai.conversations.create({
|
||||||
if (schema !== ALLOWED_SCHEMA || !ALLOWED_TABLES.has(table)) {
|
metadata: {
|
||||||
return jsonResponse(400, { error: "Table not allowed" });
|
tabla: "planes_estudio",
|
||||||
}
|
id: plan.id,
|
||||||
|
instanciador,
|
||||||
if (type !== "INSERT") {
|
},
|
||||||
return jsonResponse(200, { status: "ignored", reason: "event_type" });
|
items: [{ type: "message", role: "system", content: system_prompt }],
|
||||||
}
|
|
||||||
|
|
||||||
if (!record || typeof record !== "object") {
|
|
||||||
return jsonResponse(400, { error: "Missing record" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordId = record["id"] as string | number | undefined;
|
|
||||||
if (!recordId) {
|
|
||||||
return jsonResponse(400, { error: "Record id missing" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingConversationId = record["conversation_id"] as
|
|
||||||
| string
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
if (existingConversationId) {
|
|
||||||
return jsonResponse(200, {
|
|
||||||
status: "skipped",
|
|
||||||
conversation_id: existingConversationId,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
let conversationId: string;
|
// Crear registro en Supabase
|
||||||
|
const { data: row, error: insErr } = await supabase
|
||||||
|
.from("conversaciones_plan")
|
||||||
|
.insert({
|
||||||
|
openai_conversation_id: conv.id,
|
||||||
|
plan_estudio_id: plan.id,
|
||||||
|
estado: "ACTIVA",
|
||||||
|
})
|
||||||
|
.select("id, plan_estudio_id, openai_conversation_id, estado")
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insErr || !row) {
|
||||||
|
// rollback best-effort
|
||||||
try {
|
try {
|
||||||
conversationId = await createConversationId(openaiService, {
|
await openai.conversations.delete(conv.id);
|
||||||
table,
|
} catch (_) {}
|
||||||
record_id: String(recordId),
|
throw new HttpError(
|
||||||
});
|
500,
|
||||||
} catch (error) {
|
"db_insert_failed",
|
||||||
return jsonResponse(502, { error: (error as Error).message });
|
"No se pudo registrar la conversación",
|
||||||
|
insErr,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabase
|
return withCors(jsonResponse({ conversation_plan: row }, 201));
|
||||||
.from(table)
|
} catch (err) {
|
||||||
.update({ conversation_id: conversationId })
|
return withCors(handleErr(err));
|
||||||
.eq("id", recordId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return jsonResponse(500, { error: error.message });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(200, {
|
|
||||||
status: "updated",
|
|
||||||
table,
|
|
||||||
record_id: recordId,
|
|
||||||
conversation_id: conversationId,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
app.post(`${prefix}/asignatura/conversations`, async (c) => {
|
||||||
|
try {
|
||||||
|
const body = (await c.req.json().catch(() => ({}))) as Partial<CreateBody>;
|
||||||
|
const asignatura_id = body.asignatura_id;
|
||||||
|
assertUuid(asignatura_id ?? "", "asignatura_id");
|
||||||
|
|
||||||
|
const instanciador = body.instanciador ?? "unknown";
|
||||||
|
const system_prompt = body.system_prompt ??
|
||||||
|
"Eres un asistente experto en currículo académico. Si te piden algo ajeno a la asignatura, responde con un refusal.";
|
||||||
|
|
||||||
|
const supabase = getSupabaseServiceClient();
|
||||||
|
const openai = getOpenAI();
|
||||||
|
|
||||||
|
// 1. Verificar que la asignatura existe
|
||||||
|
const { data: asignatura, error: asigErr } = await supabase
|
||||||
|
.from("asignaturas")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", asignatura_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (asigErr || !asignatura) {
|
||||||
|
throw new HttpError(404, "asignatura_not_found", "Asignatura no encontrada");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Crear conversación en OpenAI
|
||||||
|
const conv = await openai.conversations.create({
|
||||||
|
metadata: {
|
||||||
|
tabla: "asignaturas",
|
||||||
|
id: asignatura.id,
|
||||||
|
instanciador,
|
||||||
|
},
|
||||||
|
items: [{ type: "message", role: "system", content: system_prompt }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Insertar en conversaciones_asignatura (coincidiendo con tu SQL)
|
||||||
|
const { data: row, error: insErr } = await supabase
|
||||||
|
.from("conversaciones_asignatura")
|
||||||
|
.insert({
|
||||||
|
openai_conversation_id: conv.id,
|
||||||
|
asignatura_id: asignatura.id,
|
||||||
|
estado: "ACTIVA",
|
||||||
|
conversacion_json: [], // Inicializamos como array vacío para los mensajes
|
||||||
|
// creado_por: user.id // Opcional si tienes el ID del usuario
|
||||||
|
})
|
||||||
|
.select("id, asignatura_id, openai_conversation_id, estado")
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insErr || !row) {
|
||||||
|
try { await openai.conversations.delete(conv.id); } catch (_) {}
|
||||||
|
throw new HttpError(500, "db_insert_failed", "Error al registrar conversación", insErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return withCors(jsonResponse({ conversation_asignatura: row }, 201));
|
||||||
|
} catch (err) {
|
||||||
|
return withCors(handleErr(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /conversations/:conversation_plan_id/messages
|
||||||
|
* Agrega mensaje y opcionalmente solicita respuesta estructurada (json_schema)
|
||||||
|
*/
|
||||||
|
app.post(`${prefix}/conversations/plan/:id/messages`, async (c) => {
|
||||||
|
try {
|
||||||
|
const conversation_plan_id = c.req.param("id");
|
||||||
|
assertUuid(conversation_plan_id, "conversation_plan_id");
|
||||||
|
|
||||||
|
const body = (await c.req.json().catch(() => ({}))) as Partial<AddMessageBody>;
|
||||||
|
if (!body.content || typeof body.content !== "string") {
|
||||||
|
throw new HttpError(400, "bad_input", "content es requerido");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Iniciando generación en background para mensaje_id:");
|
||||||
|
const supabase = getSupabaseServiceClient();
|
||||||
|
const svc = OpenAIService.fromEnv();
|
||||||
|
|
||||||
|
// 1. Validar existencia y estado de la conversación
|
||||||
|
const { data: row, error } = await supabase
|
||||||
|
.from("conversaciones_plan")
|
||||||
|
.select("id, openai_conversation_id, plan_estudio_id, estado, planes_estudio(*, estructuras_plan(definicion))")
|
||||||
|
.eq("id", conversation_plan_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
|
||||||
|
if (row.estado === "ARCHIVADA") throw new HttpError(409, "archived", "Conversación archivada");
|
||||||
|
|
||||||
|
const plan = (row as any).planes_estudio;
|
||||||
|
const definicion = plan?.estructuras_plan?.definicion;
|
||||||
|
const isStructured = !!definicion;
|
||||||
|
|
||||||
|
// 2. Insertar el mensaje en estado PENDIENTE
|
||||||
|
// Guardamos los metadatos necesarios para procesar la respuesta después
|
||||||
|
const { data: mensajeInsertado, error: insertErr } = await supabase
|
||||||
|
.from("plan_mensajes_ia")
|
||||||
|
.insert({
|
||||||
|
conversacion_plan_id:conversation_plan_id,
|
||||||
|
enviado_por: "00000000-0000-0000-0000-000000000000",
|
||||||
|
mensaje: body.content,
|
||||||
|
campos: body.campos ?? [],
|
||||||
|
estado: "PROCESANDO", // Estado inicial
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
|
||||||
|
|
||||||
|
// 3. Preparar Schema y Prompt
|
||||||
|
const schema = isStructured ? pickSchemaFields(definicion, body.campos ?? []) : {
|
||||||
|
type: "object",
|
||||||
|
properties: { "ai-message": { type: "string" }, "is_refusal": { type: "boolean" } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Llamada asincrónica a OpenAI con Webhook
|
||||||
|
// Nota: El SDK de OpenAI permite pasar webhooks en ciertos modelos/endpoints
|
||||||
|
console.log("mandando a openaai ");
|
||||||
|
|
||||||
|
const aiResult = await svc.createStructuredResponse({
|
||||||
|
conversation: row.openai_conversation_id,
|
||||||
|
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
|
||||||
|
background: true, // <--- ESTO ES LO QUE TE FALTABA
|
||||||
|
metadata: {
|
||||||
|
tabla: "plan_mensajes_ia",
|
||||||
|
mensaje_id: String(mensajeInsertado.id), // Siempre string
|
||||||
|
is_structured: String(isStructured)
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
format: {
|
||||||
|
type: "json_schema",
|
||||||
|
name: "definicion",
|
||||||
|
schema: schema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: [
|
||||||
|
{ role: "system", content: `Asistente de plan: ${JSON.stringify(safePlanForPrompt(plan))}` },
|
||||||
|
{ role: "user", content: body.content },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!aiResult.ok) {
|
||||||
|
throw new HttpError(500, "openai_error", "No se pudo encolar la respuesta");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Responder al cliente de inmediato
|
||||||
|
return withCors(jsonResponse({
|
||||||
|
ok: true,
|
||||||
|
mensaje_id: mensajeInsertado.id,
|
||||||
|
openai_response_id: aiResult.responseId // Para seguimiento
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return withCors(handleErr(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.post(`${prefix}/conversations/asignatura/:id/messages`, async (c) => {
|
||||||
|
try {
|
||||||
|
const conversation_asig_id = c.req.param("id");
|
||||||
|
assertUuid(conversation_asig_id, "conversation_asig_id");
|
||||||
|
|
||||||
|
const body = (await c.req.json().catch(() => ({}))) as Partial<AddMessageBody>;
|
||||||
|
if (!body.content || typeof body.content !== "string") {
|
||||||
|
throw new HttpError(400, "bad_input", "content es requerido");
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getSupabaseServiceClient();
|
||||||
|
// Usamos el servicio que ya tienes configurado para background
|
||||||
|
const svc = OpenAIService.fromEnv();
|
||||||
|
|
||||||
|
// 1. Traer datos de la asignatura
|
||||||
|
const { data: row, error } = await supabase
|
||||||
|
.from("conversaciones_asignatura")
|
||||||
|
.select(`id, openai_conversation_id, asignatura_id, asignaturas(*, estructuras_asignatura(definicion))`)
|
||||||
|
.eq("id", conversation_asig_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error || !row) throw new HttpError(404, "not_found", "Conversación no encontrada");
|
||||||
|
|
||||||
|
const asignatura = (row as any).asignaturas;
|
||||||
|
const definicion = asignatura?.estructuras_asignatura?.definicion;
|
||||||
|
const campos = body.campos ?? [];
|
||||||
|
const isStructured = !!definicion && campos.length > 0;
|
||||||
|
// 2. Insertar el mensaje en estado PROCESANDO (para que el front vea el spinner)
|
||||||
|
const { data: mensajeInsertado, error: insertErr } = await supabase
|
||||||
|
.from("asignatura_mensajes_ia")
|
||||||
|
.insert({
|
||||||
|
conversacion_asignatura_id: conversation_asig_id,
|
||||||
|
enviado_por: "00000000-0000-0000-0000-000000000000",
|
||||||
|
mensaje: body.content,
|
||||||
|
campos: body.campos ?? [],
|
||||||
|
estado: "PROCESANDO",
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insertErr) throw new HttpError(500, "db_error", "No se pudo crear el registro");
|
||||||
|
|
||||||
|
// 3. Preparar Schema (Usando tu lógica de asignatura)
|
||||||
|
const schema = isStructured
|
||||||
|
? pickSchemaAsignaturaFields(definicion, body.campos ?? [])
|
||||||
|
: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
"ai-message": { type: "string" },
|
||||||
|
"is_refusal": { type: "boolean" }
|
||||||
|
},
|
||||||
|
required: ["ai-message", "is_refusal"],
|
||||||
|
additionalProperties: false
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 4. Llamada asincrónica con background: true
|
||||||
|
const aiResult = await svc.createStructuredResponse({
|
||||||
|
conversation: row.openai_conversation_id,
|
||||||
|
model: isStructured ? CREATE_CHAT_CONVERSATION_STRUCTURED_MODELO : CREATE_CHAT_CONVERSATION_NONSTRUCTURED_MODELO,
|
||||||
|
background: true, // <--- Ahora sí, activamos el modo background
|
||||||
|
metadata: {
|
||||||
|
tabla: "asignatura_mensajes_ia", // El webhook usará esto para saber dónde hacer el UPDATE
|
||||||
|
mensaje_id: String(mensajeInsertado.id),
|
||||||
|
is_structured: String(isStructured),
|
||||||
|
conversation_id: conversation_asig_id // Extra para el webhook si lo necesita
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
format: {
|
||||||
|
type: "json_schema",
|
||||||
|
name: "mejoras_asignatura",
|
||||||
|
schema: schema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: [
|
||||||
|
{ role: "system", content: getAsignaturaSystemPrompt(asignatura, campos) },
|
||||||
|
{ 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
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return withCors(handleErr(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unknown routes
|
||||||
|
*/
|
||||||
|
app.all(
|
||||||
|
"*",
|
||||||
|
(c) =>
|
||||||
|
withCors(
|
||||||
|
jsonResponse({
|
||||||
|
error: "not_found",
|
||||||
|
message: `Route ${c.req.url} not found`,
|
||||||
|
}, 404),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleErr(err: unknown): Response {
|
||||||
|
if (err instanceof HttpError) {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: err.code, message: err.message, details: err.details ?? null },
|
||||||
|
err.status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.error("Unhandled error:", err);
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: "internal_error", message: "Unexpected error" },
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.serve(app.fetch);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export const corsHeaders: Record<string, string> = {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"Access-Control-Allow-Headers":
|
||||||
|
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform",
|
||||||
|
"access-control-allow-methods": "GET,POST,OPTIONS,DELETE",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function withCors(res: Response) {
|
||||||
|
const h = new Headers(res.headers);
|
||||||
|
for (const [k, v] of Object.entries(corsHeaders)) h.set(k, v);
|
||||||
|
return new Response(res.body, { status: res.status, headers: h });
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function mustGetEnv(name: string): string {
|
||||||
|
const v = Deno.env.get(name);
|
||||||
|
if (!v) throw new Error(`Missing env var: ${name}`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnv(name: string, fallback?: string): string | undefined {
|
||||||
|
return Deno.env.get(name) ?? fallback;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export class HttpError extends Error {
|
||||||
|
status: number;
|
||||||
|
code: string;
|
||||||
|
details?: unknown;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
status: number,
|
||||||
|
code: string,
|
||||||
|
message: string,
|
||||||
|
details?: unknown,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.code = code;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonResponse(
|
||||||
|
body: unknown,
|
||||||
|
status = 200,
|
||||||
|
headers: HeadersInit = {},
|
||||||
|
) {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import OpenAI from "npm:openai@6.16.0";
|
||||||
|
import { mustGetEnv } from "./env.ts";
|
||||||
|
|
||||||
|
export function getOpenAI() {
|
||||||
|
// OpenAI lib toma OPENAI_API_KEY de env automáticamente,
|
||||||
|
// pero lo validamos para fallar rápido:
|
||||||
|
mustGetEnv("OPENAI_API_KEY");
|
||||||
|
return new OpenAI();
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { definicionesDeEstructurasDeColumnas } from "../../_shared/estructuras.ts";
|
||||||
|
import { HttpError } from "./errors.ts";
|
||||||
|
export function pickSchemaFields(
|
||||||
|
definicion: any,
|
||||||
|
campos: string[],
|
||||||
|
) {
|
||||||
|
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
||||||
|
return definicion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extra = {
|
||||||
|
properties: {
|
||||||
|
"ai-message": {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Mensaje breve para el usuario final confirmando qué se mejoró y qué se hizo.",
|
||||||
|
examples: [
|
||||||
|
"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`.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const out = structuredClone(definicion);
|
||||||
|
|
||||||
|
// Si piden campos, filtramos propiedades/required a esos campos
|
||||||
|
const entries = Object.entries(out.properties).filter(([k]) =>
|
||||||
|
campos.includes(k)
|
||||||
|
);
|
||||||
|
out.properties = Object.fromEntries(entries);
|
||||||
|
if (Array.isArray(out.required)) {
|
||||||
|
out.required = out.required.filter((k: string) => campos.includes(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Siempre agregamos ai-message
|
||||||
|
out.properties = { ...out.properties, ...extra.properties };
|
||||||
|
out.required = Array.isArray(out.required)
|
||||||
|
? [...new Set([...out.required, ...Object.keys(extra.properties)])]
|
||||||
|
: Object.keys(extra.properties);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickSchemaAsignaturaFields(
|
||||||
|
definicion: any,
|
||||||
|
campos: string[],
|
||||||
|
) {
|
||||||
|
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
||||||
|
return definicion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extra = {
|
||||||
|
properties: {
|
||||||
|
"ai-message": {
|
||||||
|
type: "string",
|
||||||
|
description: "Tu respuesta conversacional dirigida al profesor explicando qué mejoraste.",
|
||||||
|
},
|
||||||
|
"is_refusal": {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Indica si la solicitud es inapropiada.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const out = structuredClone(definicion);
|
||||||
|
const finalProperties: Record<string, any> = {};
|
||||||
|
const finalRequired: string[] = [];
|
||||||
|
|
||||||
|
campos.forEach((key) => {
|
||||||
|
|
||||||
|
// REVISIÓN DE ESTRUCTURA ESPECIAL
|
||||||
|
if (key in definicionesDeEstructurasDeColumnas) {
|
||||||
|
// Extraemos solo el esquema del array que vive en 'x-definicion'
|
||||||
|
// Esto hace que la IA reciba el esquema del array directamente
|
||||||
|
const especial = (definicionesDeEstructurasDeColumnas as any)[key];
|
||||||
|
|
||||||
|
finalProperties[key] = especial.properties["x-definicion"];
|
||||||
|
finalRequired.push(key);
|
||||||
|
}
|
||||||
|
// CAMPOS NORMALES (los que están en 'datos')
|
||||||
|
else if (out.properties[key]) {
|
||||||
|
finalProperties[key] = out.properties[key];
|
||||||
|
if (out.required?.includes(key)) {
|
||||||
|
finalRequired.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
out.properties = { ...finalProperties, ...extra.properties };
|
||||||
|
out.required = [...new Set([...finalRequired, ...Object.keys(extra.properties)])];
|
||||||
|
out.additionalProperties = false;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safePlanForPrompt(plan: any) {
|
||||||
|
const copy = structuredClone(plan);
|
||||||
|
if (copy?.estructuras_plan) delete copy.estructuras_plan;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertUuid(v: string, name: string) {
|
||||||
|
// validación ligera
|
||||||
|
if (!v || typeof v !== "string" || v.length < 10) {
|
||||||
|
throw new HttpError(400, "bad_input", `Invalid ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeAsignaturaForPrompt(asignatura: any) {
|
||||||
|
const copy = structuredClone(asignatura);
|
||||||
|
// Eliminamos la definición de la estructura para que no ensucie el prompt
|
||||||
|
// y solo queden los datos reales de la asignatura
|
||||||
|
if (copy?.estructuras_asignatura) delete copy.estructuras_asignatura;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAsignaturaSystemPrompt(asignatura: any, campos: string[]) {
|
||||||
|
const asignaturaLimpia = safeAsignaturaForPrompt(asignatura);
|
||||||
|
const nombreAsig = asignaturaLimpia?.nombre || "la asignatura";
|
||||||
|
|
||||||
|
if (campos.length === 0) {
|
||||||
|
return `Eres un asistente experto en diseño curricular.
|
||||||
|
Tu objetivo es ayudar al profesor a mejorar su asignatura: "${nombreAsig}".
|
||||||
|
DATOS ACTUALES: ${JSON.stringify(asignaturaLimpia)}.
|
||||||
|
|
||||||
|
COMPORTAMIENTO:
|
||||||
|
- El usuario aún no ha seleccionado campos específicos para mejorar.
|
||||||
|
- NO propongas cambios técnicos detallados ni rellenes campos del JSON todavía.
|
||||||
|
- Saluda cordialmente y menciona qué partes de esta asignatura puedes ayudar a mejorar (objetivos, contenidos, criterios, etc.).
|
||||||
|
- Mantén una conversación fluida y espera a que el usuario elija qué quiere trabajar.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Eres un asistente experto en diseño curricular trabajando sobre: "${nombreAsig}".
|
||||||
|
DATOS ACTUALES: ${JSON.stringify(asignaturaLimpia)}.
|
||||||
|
|
||||||
|
TAREA CRÍTICA:
|
||||||
|
El usuario ha solicitado mejorar estos ${campos.length} campos: ${campos.join(", ")}.
|
||||||
|
|
||||||
|
REGLAS DE ORO:
|
||||||
|
1. Debes proporcionar una propuesta de mejora para CADA UNO de los campos solicitados.
|
||||||
|
2. No omitas ninguno. Si un campo no requiere cambios drásticos, optimiza su redacción técnica.
|
||||||
|
3. En el JSON, cada campo debe contener tu propuesta de texto mejorado.
|
||||||
|
4. En 'ai-message', resume los cambios hechos en cada uno de los campos solicitados.`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { createClient } from "jsr:@supabase/supabase-js";
|
||||||
|
import { mustGetEnv } from "./env.ts";
|
||||||
|
import { HttpError } from "./errors.ts";
|
||||||
|
|
||||||
|
export function getSupabaseServiceClient() {
|
||||||
|
const url = mustGetEnv("SUPABASE_URL");
|
||||||
|
const key = mustGetEnv("SUPABASE_SERVICE_ROLE_KEY");
|
||||||
|
return createClient(url, key, { auth: { persistSession: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSupabaseAnonClient(authHeader?: string) {
|
||||||
|
const url = mustGetEnv("SUPABASE_URL");
|
||||||
|
const key = mustGetEnv("SUPABASE_ANON_KEY");
|
||||||
|
return createClient(url, key, {
|
||||||
|
auth: { persistSession: false },
|
||||||
|
global: authHeader ? { headers: { Authorization: authHeader } } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireUser(authHeader?: string) {
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new HttpError(401, "missing_auth", "Missing Authorization header");
|
||||||
|
}
|
||||||
|
const anon = getSupabaseAnonClient(authHeader);
|
||||||
|
const { data, error } = await anon.auth.getUser();
|
||||||
|
if (error || !data?.user) {
|
||||||
|
throw new HttpError(401, "invalid_auth", "Invalid or expired token", error);
|
||||||
|
}
|
||||||
|
return data.user;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,9 @@
|
|||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"zod": "npm:zod@4.3.5",
|
||||||
|
"openai": "npm:openai@6.16.0",
|
||||||
|
"openai/helpers/zod": "npm:openai@6.16.0/helpers/zod",
|
||||||
|
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
||||||
|
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
import { corsHeaders } from "../_shared/cors.ts";
|
||||||
|
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import type { Database, Tables } from "../_shared/database.types.ts";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { zodTextFormat } from "openai/helpers/zod";
|
||||||
|
import {
|
||||||
|
OpenAIService,
|
||||||
|
type StructuredResponseOptions,
|
||||||
|
} from "../_shared/openai-service.ts";
|
||||||
|
|
||||||
|
addEventListener("beforeunload", (ev: any) => {
|
||||||
|
console.error(
|
||||||
|
"ALERTA: La función se va a apagar. Razón:",
|
||||||
|
ev?.detail?.reason,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DataAsignaturaSugerida = {
|
||||||
|
nombre: Tables<"asignaturas">["nombre"];
|
||||||
|
codigo?: Tables<"asignaturas">["codigo"];
|
||||||
|
tipo: Tables<"asignaturas">["tipo"] | null;
|
||||||
|
creditos: Tables<"asignaturas">["creditos"] | null;
|
||||||
|
horasAcademicas?: number | null;
|
||||||
|
horasIndependientes?: number | null;
|
||||||
|
descripcion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AsignaturaSugeridaItemSchema: z.ZodType<DataAsignaturaSugerida> = z
|
||||||
|
.object({
|
||||||
|
nombre: z.string().describe("Nombre de la asignatura a crear"),
|
||||||
|
codigo: z.string().optional().nullable().describe(
|
||||||
|
"Código o clave de la asignatura. Un string único que la identifique, como 'MAT101' o 'FIS202'. Opcional, pero recomendado para evitar confusiones.",
|
||||||
|
),
|
||||||
|
tipo: z.enum(["TRONCAL", "OBLIGATORIA", "OPTATIVA", "OTRA"]).nullable(),
|
||||||
|
creditos: z.number().nullable(),
|
||||||
|
horasAcademicas: z.number().optional().nullable(),
|
||||||
|
horasIndependientes: z.number().optional().nullable(),
|
||||||
|
descripcion: z.string().max(200),
|
||||||
|
});
|
||||||
|
|
||||||
|
const RequestSchema = z.object({
|
||||||
|
plan_estudio_id: z.string().uuid(),
|
||||||
|
// numero_de_ciclo: z.number().int().positive(), // ya no se usa
|
||||||
|
enfoque: z.string().trim().min(1).optional(),
|
||||||
|
cantidad_de_sugerencias: z.number().int().positive().max(15),
|
||||||
|
sugerencias_conservadas: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
nombre: z.string().trim().min(1),
|
||||||
|
descripcion: z.string().trim().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
function normalizeName(value: string): string {
|
||||||
|
return value
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/\p{Diacritic}/gu, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const method = req.method;
|
||||||
|
if (method !== "POST") {
|
||||||
|
throw new HttpError(
|
||||||
|
405,
|
||||||
|
"Método no permitido.",
|
||||||
|
"METHOD_NOT_ALLOWED",
|
||||||
|
{ method },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeaderRaw = req.headers.get("Authorization") ??
|
||||||
|
req.headers.get("authorization");
|
||||||
|
if (!authHeaderRaw) {
|
||||||
|
throw new HttpError(
|
||||||
|
401,
|
||||||
|
"No autorizado.",
|
||||||
|
"UNAUTHORIZED",
|
||||||
|
{ reason: "missing_authorization_header" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawBody: unknown;
|
||||||
|
try {
|
||||||
|
rawBody = await req.json();
|
||||||
|
} catch (e) {
|
||||||
|
throw new HttpError(
|
||||||
|
400,
|
||||||
|
"Body JSON inválido.",
|
||||||
|
"INVALID_JSON",
|
||||||
|
{ cause: e },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validated = RequestSchema.safeParse(rawBody);
|
||||||
|
if (!validated.success) {
|
||||||
|
throw new HttpError(
|
||||||
|
422,
|
||||||
|
formatZodIssues(validated.error.issues),
|
||||||
|
"VALIDATION_ERROR",
|
||||||
|
validated.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
plan_estudio_id,
|
||||||
|
// numero_de_ciclo,
|
||||||
|
enfoque,
|
||||||
|
cantidad_de_sugerencias,
|
||||||
|
sugerencias_conservadas,
|
||||||
|
} = validated.data;
|
||||||
|
|
||||||
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
||||||
|
const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
||||||
|
if (!SUPABASE_URL || !SERVICE_ROLE_KEY) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Configuración del servidor incompleta.",
|
||||||
|
"MISSING_ENV",
|
||||||
|
{
|
||||||
|
missing: [
|
||||||
|
!SUPABASE_URL ? "SUPABASE_URL" : null,
|
||||||
|
!SERVICE_ROLE_KEY ? "SUPABASE_SERVICE_ROLE_KEY" : null,
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabaseService = createClient<Database>(
|
||||||
|
SUPABASE_URL,
|
||||||
|
SERVICE_ROLE_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Model name controlled via env var
|
||||||
|
const GENERATE_SUBJECT_SUGGESTIONS_MODELO = Deno.env.get(
|
||||||
|
"GENERATE_SUBJECT_SUGGESTIONS_MODELO",
|
||||||
|
) ?? "gpt-5-mini";
|
||||||
|
|
||||||
|
const { data: plan, error: planError } = await supabaseService
|
||||||
|
.from("planes_estudio")
|
||||||
|
.select("id,nombre,nivel,tipo_ciclo,numero_ciclos,datos")
|
||||||
|
.eq("id", plan_estudio_id)
|
||||||
|
.single();
|
||||||
|
if (planError) {
|
||||||
|
const maybeCode = (planError as { code?: string }).code;
|
||||||
|
if (maybeCode === "PGRST116") {
|
||||||
|
throw new HttpError(
|
||||||
|
404,
|
||||||
|
"No se encontró el plan de estudio.",
|
||||||
|
"NOT_FOUND",
|
||||||
|
{ table: "planes_estudio", id: plan_estudio_id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"No se pudo obtener el plan de estudio.",
|
||||||
|
"SUPABASE_QUERY_FAILED",
|
||||||
|
planError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: asignaturas, error: asignaturasError } = await supabaseService
|
||||||
|
.from("asignaturas")
|
||||||
|
.select("id,nombre,numero_ciclo,codigo,tipo,creditos")
|
||||||
|
.eq("plan_estudio_id", plan_estudio_id);
|
||||||
|
if (asignaturasError) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"No se pudieron obtener las asignaturas del plan de estudio.",
|
||||||
|
"SUPABASE_QUERY_FAILED",
|
||||||
|
asignaturasError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingNames = new Set(
|
||||||
|
(asignaturas ?? [])
|
||||||
|
.map((a) => normalizeName(a.nombre))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
const conservedNames = new Set(
|
||||||
|
(sugerencias_conservadas ?? [])
|
||||||
|
.map((s) => normalizeName(s.nombre))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
const forbiddenNames = Array.from(
|
||||||
|
new Set([...existingNames, ...conservedNames]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const asignaturasResumen = (asignaturas ?? [])
|
||||||
|
.map((a) => {
|
||||||
|
const ciclo = a.numero_ciclo == null ? "(sin ciclo)" : a.numero_ciclo;
|
||||||
|
const codigo = a.codigo ? ` - ${a.codigo}` : "";
|
||||||
|
return `- [ciclo ${ciclo}] ${a.nombre}${codigo}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const sugerenciasConservadasResumen = (sugerencias_conservadas ?? [])
|
||||||
|
.map((s) => `- ${s.nombre}: ${s.descripcion}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const systemPrompt =
|
||||||
|
"Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el esquema proporcionado.";
|
||||||
|
|
||||||
|
const userPrompt =
|
||||||
|
`Necesito sugerencias NUEVAS de asignaturas para un plan de estudios.\n\n` +
|
||||||
|
`Plan de estudio:\n` +
|
||||||
|
`- id: ${plan.id}\n` +
|
||||||
|
`- nombre: ${plan.nombre}\n` +
|
||||||
|
`- nivel: ${plan.nivel}\n` +
|
||||||
|
`- tipo_ciclo: ${plan.tipo_ciclo}\n` +
|
||||||
|
`- numero_ciclos: ${plan.numero_ciclos}\n\n` +
|
||||||
|
`Datos del plan (JSON):\n${JSON.stringify(plan.datos)}\n\n` +
|
||||||
|
`Asignaturas existentes en el plan (NO repetir):\n${
|
||||||
|
asignaturasResumen || "(ninguna)"
|
||||||
|
}\n\n` +
|
||||||
|
`Sugerencias conservadas por el usuario (NO repetir):\n${
|
||||||
|
sugerenciasConservadasResumen || "(ninguna)"
|
||||||
|
}\n\n` +
|
||||||
|
`Enfoque (opcional): ${enfoque ?? "(ninguno)"}\n\n` +
|
||||||
|
`Requisitos estrictos:\n` +
|
||||||
|
`1) Genera EXACTAMENTE ${cantidad_de_sugerencias} sugerencias.\n` +
|
||||||
|
`2) No repitas nombres que ya existan en el plan ni los nombres de las sugerencias conservadas.\n` +
|
||||||
|
`3) Tampoco repitas nombres entre tus nuevas sugerencias.\n` +
|
||||||
|
`4) Evita nombres demasiado similares (diferencias solo por mayúsculas, tildes, signos o palabras triviales).\n` +
|
||||||
|
`5) Cada sugerencia debe incluir un nombre y una descripción clara y útil (sin pasarse del límite de 200 caracteres).\n\n` +
|
||||||
|
`Lista de nombres prohibidos (normalizados):\n` +
|
||||||
|
forbiddenNames.map((n) => `- ${n}`).join("\n");
|
||||||
|
|
||||||
|
const AsignaturaSugeridaSchema = z
|
||||||
|
.object({
|
||||||
|
sugerencias: z
|
||||||
|
.array(AsignaturaSugeridaItemSchema)
|
||||||
|
.length(cantidad_de_sugerencias)
|
||||||
|
.describe(
|
||||||
|
`Arreglo de ${cantidad_de_sugerencias} sugerencias de asignatura`,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.describe(
|
||||||
|
`Respuesta estructurada con ${cantidad_de_sugerencias} sugerencias de asignatura`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const svc = OpenAIService.fromEnv();
|
||||||
|
if (!(svc instanceof OpenAIService)) {
|
||||||
|
throw new HttpError(
|
||||||
|
500,
|
||||||
|
"Configuración del servidor incompleta.",
|
||||||
|
"OPENAI_MISCONFIGURED",
|
||||||
|
svc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: StructuredResponseOptions = {
|
||||||
|
model: GENERATE_SUBJECT_SUGGESTIONS_MODELO,
|
||||||
|
input: [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "input_text",
|
||||||
|
text: userPrompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
text: {
|
||||||
|
format: zodTextFormat(
|
||||||
|
AsignaturaSugeridaSchema,
|
||||||
|
"estructura_asignaturas",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiResult = await svc.createStructuredResponse<
|
||||||
|
typeof AsignaturaSugeridaSchema._output
|
||||||
|
>(
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (!aiResult.ok) {
|
||||||
|
const status = aiResult.code === "MissingEnv" ? 500 : 502;
|
||||||
|
throw new HttpError(
|
||||||
|
status,
|
||||||
|
"No se pudieron generar sugerencias con IA.",
|
||||||
|
"OPENAI_REQUEST_FAILED",
|
||||||
|
aiResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = aiResult.output ?? null;
|
||||||
|
if (output == null && aiResult.outputText) {
|
||||||
|
try {
|
||||||
|
output = 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (output == null) {
|
||||||
|
throw new HttpError(
|
||||||
|
502,
|
||||||
|
"La respuesta de la IA no contiene salida estructurada.",
|
||||||
|
"OPENAI_MISSING_STRUCTURED_OUTPUT",
|
||||||
|
{ outputText: aiResult.outputText ?? null },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = AsignaturaSugeridaSchema.safeParse(output);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new HttpError(
|
||||||
|
502,
|
||||||
|
"La salida estructurada no coincide con el esquema esperado.",
|
||||||
|
"OPENAI_SCHEMA_MISMATCH",
|
||||||
|
parsed.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${
|
||||||
|
new Date().toISOString()
|
||||||
|
}][${functionName}]: Request processed successfully`,
|
||||||
|
);
|
||||||
|
return sendSuccess(parsed.data.sugerencias);
|
||||||
|
} 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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));
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// supabase/functions/ai-structured/ai_structured.test.ts
|
// supabase/functions/ai-structured/ai_structured.test.ts
|
||||||
|
// dummy number
|
||||||
/// <reference lib="deno.ns" />
|
/// <reference lib="deno.ns" />
|
||||||
/// <reference lib="dom" />
|
/// <reference lib="dom" />
|
||||||
/// <reference lib="dom.iterable" />
|
/// <reference lib="dom.iterable" />
|
||||||
@@ -160,6 +161,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));
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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,5 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
SET statement_timeout = 0;
|
SET statement_timeout = 0;
|
||||||
SET lock_timeout = 0;
|
SET lock_timeout = 0;
|
||||||
SET idle_in_transaction_session_timeout = 0;
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
@@ -55,16 +54,6 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
|
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
|
||||||
|
|
||||||
|
|
||||||
@@ -86,6 +75,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',
|
||||||
@@ -387,10 +398,6 @@ begin
|
|||||||
end if;
|
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
|
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
||||||
if v_response_id is not null then
|
if v_response_id is not null then
|
||||||
@@ -491,7 +498,6 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
|||||||
"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,
|
||||||
@@ -503,10 +509,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,
|
"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)))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -563,10 +573,6 @@ CREATE TABLE IF NOT EXISTS "public"."cambios_plan" (
|
|||||||
ALTER TABLE "public"."cambios_plan" OWNER TO "postgres";
|
ALTER TABLE "public"."cambios_plan" OWNER TO "postgres";
|
||||||
|
|
||||||
|
|
||||||
COMMENT ON COLUMN "public"."cambios_plan"."response_id" IS 'El ID de respuesta de OpenAI';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "public"."carreras" (
|
CREATE TABLE IF NOT EXISTS "public"."carreras" (
|
||||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||||
"facultad_id" "uuid" NOT NULL,
|
"facultad_id" "uuid" NOT NULL,
|
||||||
@@ -582,6 +588,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,
|
||||||
@@ -704,7 +744,6 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" (
|
|||||||
"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,
|
"plan_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED,
|
||||||
"conversation_id" "text",
|
|
||||||
CONSTRAINT "planes_estudio_numero_ciclos_check" CHECK (("numero_ciclos" > 0))
|
CONSTRAINT "planes_estudio_numero_ciclos_check" CHECK (("numero_ciclos" > 0))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -712,10 +751,6 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" (
|
|||||||
ALTER TABLE "public"."planes_estudio" OWNER TO "postgres";
|
ALTER TABLE "public"."planes_estudio" OWNER TO "postgres";
|
||||||
|
|
||||||
|
|
||||||
COMMENT ON COLUMN "public"."planes_estudio"."conversation_id" IS 'ID de conversación de OpenAI';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW "public"."plantilla_plan" AS
|
CREATE OR REPLACE VIEW "public"."plantilla_plan" AS
|
||||||
SELECT "plan"."id" AS "plan_estudio_id",
|
SELECT "plan"."id" AS "plan_estudio_id",
|
||||||
"struct"."id" AS "estructura_id",
|
"struct"."id" AS "estructura_id",
|
||||||
@@ -848,6 +883,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");
|
||||||
|
|
||||||
@@ -898,11 +953,6 @@ ALTER TABLE ONLY "public"."notificaciones"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE ONLY "public"."planes_estudio"
|
|
||||||
ADD CONSTRAINT "planes_estudio_conversation_id_key" UNIQUE ("conversation_id");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE ONLY "public"."planes_estudio"
|
ALTER TABLE ONLY "public"."planes_estudio"
|
||||||
ADD CONSTRAINT "planes_estudio_pkey" PRIMARY KEY ("id");
|
ADD CONSTRAINT "planes_estudio_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
@@ -979,6 +1029,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");
|
||||||
|
|
||||||
|
|
||||||
@@ -1087,6 +1153,36 @@ ALTER TABLE ONLY "public"."carreras"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -1215,10 +1311,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";
|
||||||
@@ -1453,6 +1545,12 @@ GRANT USAGE ON SCHEMA "public" TO "service_role";
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1572,6 +1670,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";
|
||||||
@@ -1727,4 +1837,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;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
alter publication supabase_realtime add table asignatura_mensajes_ia;
|
||||||
|
|
||||||
|
alter publication supabase_realtime add table plan_mensajes_ia;
|
||||||
|
|
||||||
|
alter publication supabase_realtime add table asignaturas;
|
||||||
|
|
||||||
|
alter publication supabase_realtime add table planes_estudio;
|
||||||
|
|
||||||
|
-- Comentario para probar el trigger 3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."conversaciones_asignatura" add column "nombre" text default ('Chat '::text || CURRENT_DATE);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
alter table "public"."bibliografia_asignatura" drop column "biblioteca_item_id";
|
||||||
|
|
||||||
|
alter table "public"."bibliografia_asignatura" drop column "tipo_fuente";
|
||||||
|
|
||||||
|
alter table "public"."bibliografia_asignatura" add column "referencia_biblioteca" text;
|
||||||
|
|
||||||
|
alter table "public"."bibliografia_asignatura" add column "referencia_en_linea" text;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
create extension if not exists "pgjwt" with schema "extensions";
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user