@envisiongroup/create-alakazam 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +56 -0
  3. package/bin/create-alakazam.mjs +7 -0
  4. package/package.json +28 -0
  5. package/src/args.mjs +72 -0
  6. package/src/createAlakazamCli.mjs +115 -0
  7. package/src/projectName.mjs +33 -0
  8. package/src/prompts.mjs +63 -0
  9. package/src/registry.mjs +62 -0
  10. package/src/template.mjs +135 -0
  11. package/templates/default/.env.sample +55 -0
  12. package/templates/default/README.md +66 -0
  13. package/templates/default/gitignore +7 -0
  14. package/templates/default/package.json +36 -0
  15. package/templates/default/src/mastra/access/role-rules.ts +31 -0
  16. package/templates/default/src/mastra/agents/azure-claude-sandbox-agent/azure-claude-sandbox-agent.ts +108 -0
  17. package/templates/default/src/mastra/agents/azure-openai-sandbox-agent/azure-openai-sandbox-agent.ts +111 -0
  18. package/templates/default/src/mastra/agents/azure-openai-sandbox-agent/prompts/index.ts +42 -0
  19. package/templates/default/src/mastra/agents/user-context-agent/prompts/index.ts +93 -0
  20. package/templates/default/src/mastra/agents/user-context-agent/tools/get-my-profile-tool.ts +62 -0
  21. package/templates/default/src/mastra/agents/user-context-agent/user-context-agent.ts +87 -0
  22. package/templates/default/src/mastra/agents/weather-agent/weather-agent.ts +68 -0
  23. package/templates/default/src/mastra/index.ts +47 -0
  24. package/templates/default/src/mastra/scorers/weather-scorer.ts +92 -0
  25. package/templates/default/src/mastra/tools/weather-tool.ts +110 -0
  26. package/templates/default/src/mastra/workflows/weather-workflow.ts +198 -0
  27. package/templates/default/tsconfig.json +16 -0
@@ -0,0 +1,66 @@
1
+ # **PROJECT_NAME**
2
+
3
+ Agents hub [Mastra](https://mastra.ai) construido sobre
4
+ [`@envisiongroup/alakazam`](https://www.npmjs.com/package/@envisiongroup/alakazam).
5
+
6
+ Todo el wiring transversal (auth Entra ID, storage multi-dialecto, rutas de
7
+ chat/catálogo/adjuntos, observability, CORS) lo resuelve `createAgentsHub()`;
8
+ este repo solo declara su dominio en `src/mastra/`.
9
+
10
+ ## Setup
11
+
12
+ ```bash
13
+ cp .env.sample .env # rellena AZURE_MODEL_*, AZURE_API_* y OPENAI_API_KEY
14
+ pnpm install
15
+ pnpm dev # Mastra Studio en http://localhost:4111
16
+ ```
17
+
18
+ > **npm privado**: `@envisiongroup/alakazam` es un paquete restringido del scope
19
+ > `@envisiongroup`. Necesitas un token npm con acceso al scope configurado en tu
20
+ > `~/.npmrc` (`//registry.npmjs.org/:_authToken=...`) antes de `pnpm install`.
21
+
22
+ ## Qué incluye el template
23
+
24
+ | Recurso | Demuestra |
25
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | `agents/weather-agent` | Agente mínimo: `hubAgent()` (catálogo + acceso público), `getAzureClient()`, tool propia, scorers, `Memory`. |
27
+ | `agents/azure-openai-sandbox-agent` | Patrones avanzados: emisión de reasoning (`emitReasoning: true` en `hubAgent()` + `reasoningSummary`), adjuntos con `fileAttachmentProcessor` (uploads), generación de archivos con `code_interpreter` y links de descarga (downloads), `capabilities` vía `hubAgent()`. |
28
+ | `agents/azure-claude-sandbox-agent` | Provider alternativo: Claude vía Azure AI Foundry (capa de compatibilidad Responses API), adjuntos PDF/imágenes nativos, fail-closed de uploads Office (sin `code_interpreter`). |
29
+ | `agents/user-context-agent` | Contexto del usuario autenticado: `requestContextSchema`, instrucciones dinámicas con la identidad del usuario y tools condicionales según haya (o no) usuario en el `requestContext`. Sin Microsoft Graph (el profile resolver es Noop sin env vars → funciona out-of-the-box). |
30
+ | `tools/weather-tool` | `createTool` con schemas zod contra una API pública. |
31
+ | `workflows/weather-workflow` | `createWorkflow` + `createStep` con schemas zod y metadata `hubWorkflow()` (catálogo + acceso). |
32
+ | `scorers/weather-scorer` | Scorers prebuilt de `@mastra/evals` + un scorer LLM-judged custom. |
33
+ | `access/role-rules.ts` | Dónde mapear claims del IdP (grupos Entra, dominios, emails) → roles de aplicación. |
34
+
35
+ Si generaste el proyecto con `--minimal`, solo queda la suite weather (agente,
36
+ tool, workflow y scorer): los tres agents de ejemplo avanzados no se copian.
37
+
38
+ > **Adjuntos en Mastra Studio (limitación conocida)**: el composer del chat de
39
+ > Studio solo envía `image/*`, PDF, video y audio como _file parts_. Los
40
+ > archivos Office/CSV (DOCX, XLSX, PPTX, CSV) los lee como texto y llegan
41
+ > corruptos al modelo — es un bug de Mastra upstream (vivo en `main` y en
42
+ > `mastra@1.19.0`, sin fix publicado). Para probar el flujo de adjuntos
43
+ > Office de `azure-openai-sandbox-agent` end-to-end usa el SPA autenticado;
44
+ > en Studio puedes probar el pipeline de adjuntos con PDF.
45
+
46
+ ## Convenciones clave
47
+
48
+ - **Acceso por recurso**: todo agent/workflow registrado declara su acceso en
49
+ `metadata.envisionHub` vía `hubAgent()` — sin eso el arranque falla
50
+ (default-deny, fail-fast).
51
+ - **Imports del paquete**: usa los subpaths del exports map, p. ej.
52
+ `@envisiongroup/alakazam/services/azure-model`. Los archivos internos no
53
+ son importables.
54
+ - **Peers**: `@mastra/*`, `@ai-sdk/*`, `ai` y `zod` son peerDependencies de
55
+ alakazam (una sola instancia de Mastra para no romper singletons). Los
56
+ dialectos de storage (`pg`, `mssql`) son peers opcionales: instálalos solo
57
+ si cambias el `DATABASE_URL` por defecto.
58
+
59
+ ## Scripts
60
+
61
+ ```bash
62
+ pnpm dev # desarrollo con Mastra Studio
63
+ pnpm build # build de producción (mastra build)
64
+ pnpm start # servir el build
65
+ pnpm typecheck # tsc --noEmit
66
+ ```
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ .mastra/
4
+ .env
5
+ *.db
6
+ *.db-journal
7
+ .DS_Store
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "description": "Agents hub Mastra construido sobre @envisiongroup/alakazam.",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.13.0"
9
+ },
10
+ "scripts": {
11
+ "dev": "mastra dev",
12
+ "build": "mastra build",
13
+ "start": "mastra start",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "@envisiongroup/alakazam": "__ALAKAZAM_VERSION__",
18
+ "@ai-sdk/azure": "^3.0.51",
19
+ "@ai-sdk/openai": "^3.0.49",
20
+ "@mastra/ai-sdk": "^1.6.2",
21
+ "@mastra/core": "^1.51.0",
22
+ "@mastra/editor": "^0.13.7",
23
+ "@mastra/evals": "^1.5.1",
24
+ "@mastra/libsql": "^1.16.0",
25
+ "@mastra/loggers": "^1.2.0",
26
+ "@mastra/memory": "^1.23.0",
27
+ "@mastra/observability": "^1.16.1",
28
+ "ai": "^6.0.149",
29
+ "zod": "^4.3.6"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^25.5.0",
33
+ "mastra": "^1.19.0",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }
@@ -0,0 +1,31 @@
1
+ import type { RoleRule } from '@envisiongroup/alakazam';
2
+
3
+ /**
4
+ * Reglas del proveedor de identidad → roles de aplicación de este cliente.
5
+ *
6
+ * Este es el único lugar del cliente que conoce datos del proveedor de
7
+ * identidad, como grupos de Entra, dominios y correos. Los agentes y
8
+ * workflows declaran roles lógicos en `metadata.envisionHub.access`; aquí se
9
+ * define quién recibe esos roles según sus claims.
10
+ *
11
+ * Estado actual: todos los recursos invocables del hub son `{ public: true }`
12
+ * o `{ internal: true }`, así que no hacen falta reglas. Cuando un recurso
13
+ * declare `access: { roles: ['mi-rol'] }`, agregar la regla que lo otorga:
14
+ *
15
+ * ```ts
16
+ * // Grupo de Entra ID (Object ID del grupo) → rol de aplicación:
17
+ * { when: { provider: 'entra', group: '8f14e45f-…' }, grant: 'cochilco-viewer' },
18
+ * // Dominio (tid Entra / hd Google Workspace) → rol:
19
+ * { when: { provider: 'google', domain: 'envisiongroup.com' }, grant: 'employee' },
20
+ * // Email directo → rol (útil para admins puntuales):
21
+ * { when: { email: 'admin@envisiongroup.com' }, grant: 'hub-admin' },
22
+ * ```
23
+ *
24
+ * Requiere que la App Registration de la API tenga configurado
25
+ * `groupMembershipClaims` (p. ej. "SecurityGroup") para que el JWT incluya
26
+ * el claim `groups`. El Object ID del grupo se obtiene en
27
+ * Entra ID → Groups → propiedad "Object ID".
28
+ */
29
+ export const ROLE_RULES: RoleRule[] = [
30
+ // { when: { provider: 'entra', group: '<OBJECT-ID-DEL-GRUPO>' }, grant: 'mi-rol' },
31
+ ];
@@ -0,0 +1,108 @@
1
+ import { Agent } from '@mastra/core/agent';
2
+ import { Memory } from '@mastra/memory';
3
+ import { getAzureClient } from '@envisiongroup/alakazam/services/azure-model';
4
+ import {
5
+ fileAttachmentProcessor,
6
+ uploadGrantStore,
7
+ uploadProviderRegistry,
8
+ } from '@envisiongroup/alakazam/services/uploads';
9
+ import { attachments, hubAgent } from '@envisiongroup/alakazam';
10
+
11
+ // Crea el cliente de Azure y selecciona el modelo LLM que se usara.
12
+ const azureClient = getAzureClient();
13
+
14
+ // Claude se expone mediante Azure AI Foundry. Usamos `getAzureClient` en lugar
15
+ // de `@ai-sdk/anthropic` porque Microsoft ofrece una capa de compatibilidad
16
+ // sobre los deployments de Foundry. Esta capa recibe el formato de Azure OpenAI
17
+ // Responses API y lo convierte internamente al formato de Anthropic Messages.
18
+ // Consideraciones:
19
+ //
20
+ // + Reutilizamos el mismo SDK y las variables de entorno que gpt-*.
21
+ // + Los PDF llegan mediante Azure Files (`file_id`), no como base64 inline.
22
+ // Esto reduce la latencia y el consumo de tokens.
23
+ // + `provider-selector` resuelve `azure.responses` como `azure-files`, por lo
24
+ // que también se aplican las validaciones de seguridad y deduplicación.
25
+ // - Algunas funciones de Claude no están disponibles en esta capa: la visión
26
+ // puede ser inconsistente, y no se exponen `extended_thinking` ni el
27
+ // almacenamiento temporal de prompts con TTL personalizado.
28
+ // - Un cambio en el conversor de Microsoft podría afectar este agente.
29
+ //
30
+ // Si necesitas OCR de imágenes, visión avanzada, `extended_thinking`, cache de
31
+ // prompts, citas u otra función exclusiva de Claude, usa el SDK nativo
32
+ // `@ai-sdk/anthropic` con `getAnthropicClient`. Esa opción requiere configurar
33
+ // `AZURE_ANTHROPIC_BASE_URL`.
34
+ const llmModel = azureClient('claude-opus-4-7');
35
+
36
+ /**
37
+ * Agente de prueba general para Claude. Pensado para experimentar con
38
+ * adjuntos (PDFs principalmente) y validar features del flujo del modulo de
39
+ * uploads contra el modelo. NO tiene proposito de negocio especifico — para
40
+ * ese tipo de uso, crear un agente dedicado.
41
+ */
42
+ export const azureClaudeSandboxAgent = new Agent({
43
+ id: 'azure-claude-sandbox-agent',
44
+ name: 'Sandbox Azure Claude',
45
+ description:
46
+ 'Agente sandbox para probar Claude (via Azure AI Foundry) con adjuntos. Acepta PDFs y los lee nativamente. Sin caso de uso especifico — para experimentar con prompts, adjuntos y comportamiento del modelo.',
47
+ instructions: `
48
+ Eres un asistente de proposito general construido sobre Claude.
49
+
50
+ # Como respondes
51
+ - En español, claro y conciso. Sin formalismos innecesarios.
52
+ - Si el usuario te hace una pregunta puntual, contesta directo. No agregues estructura tipo "1, 2, 3" si no aporta.
53
+ - Si el usuario te pide algo que requiere mas analisis (resumir un documento, comparar, extraer datos), si conviene estructurar la respuesta — usa secciones / listas / tablas markdown segun ayude.
54
+
55
+ # Adjuntos
56
+ - PDFs e imagenes: el usuario puede adjuntar estos archivos y vos los lees directamente como contenido visual. No tenes sandbox de Python.
57
+ - Para cualquier calculo numerico que hagas sobre tablas del PDF o imagen, mostra el desglose paso a paso para que el usuario pueda auditar. No alucines cifras: si una celda no se lee bien, decilo.
58
+ - Otros formatos (XLSX/DOCX/PPTX/CSV): si el usuario adjunta algo asi, indicale que hoy este agente solo soporta PDFs e imagenes. No intentes adivinar contenido.
59
+
60
+ # Limites
61
+ - No tenes acceso a internet ni a tools externas.
62
+ - No podes generar archivos binarios (Excel, Word, imagenes). Si el usuario los pide, ofrece el contenido como markdown / CSV / texto inline.
63
+ - Si una pregunta requiere informacion que no tenes en el contexto, decilo en vez de inventar.
64
+ `,
65
+ model: llmModel,
66
+ metadata: {
67
+ ...hubAgent({
68
+ catalog: { tags: ['sandbox', 'claude', 'azure'] },
69
+ access: { public: true },
70
+ capabilities: {
71
+ attachments: [attachments.pdf, attachments.images],
72
+ },
73
+ }),
74
+ },
75
+ // No se declara `code_interpreter`: aunque Claude se accede via la capa de
76
+ // compatibilidad de Azure OpenAI Responses API, el tool `code_interpreter`
77
+ // NO esta soportado para deployments de Claude (es feature OpenAI-only).
78
+ // Sin esta tool declarada, el `fileAttachmentProcessor` entra en
79
+ // fail-closed para Office docs y no intenta subirlos.
80
+ inputProcessors: [
81
+ // Intercepta `FileUIPart` inline del usuario. Como `model.provider` es
82
+ // `azure.responses` (capa de compat), el `provider-selector` resuelve
83
+ // `azure-files`. Resultado por tipo de adjunto:
84
+ //
85
+ // - PDF: sube a Azure Files API, emite marker firmado con HMAC,
86
+ // `processLLMRequest` lo reinyecta como `input_file.file_id`. La
87
+ // capa de compat de Foundry traduce eso a Claude `document` block.
88
+ // - XLSX/DOCX/PPTX/CSV: delivery seria `code-interpreter-container`,
89
+ // pero como NO pasamos `codeInterpreterFactory` el processor entra
90
+ // en fail-closed → devuelve la FileUIPart original. El AI SDK la
91
+ // rechaza con `AI_UnsupportedFunctionalityError`, error claro.
92
+ // - Imagen: no esta en el whitelist actual (`SUPPORTED_UPLOAD_FORMATS`)
93
+ // → fluye inline. Resultado depende de si la capa de compat de
94
+ // Foundry traduce bien `input_image` a Claude `image` block (a
95
+ // veces si, a veces no — feature en evolucion).
96
+ fileAttachmentProcessor({
97
+ grantStore: uploadGrantStore,
98
+ providerRegistry: uploadProviderRegistry,
99
+ model: llmModel,
100
+ // SIN `codeInterpreterFactory`: Claude no soporta esa tool. El
101
+ // fail-closed del processor evita uploads inutiles a Azure para
102
+ // Office docs.
103
+ }),
104
+ ],
105
+ // SIN outputProcessors: no hay `code_interpreter` generando archivos para
106
+ // descargar.
107
+ memory: new Memory(),
108
+ });
@@ -0,0 +1,111 @@
1
+ import { Agent } from '@mastra/core/agent';
2
+ import { Memory } from '@mastra/memory';
3
+ import { getAzureClient } from '@envisiongroup/alakazam/services/azure-model';
4
+ import { attachments, hubAgent } from '@envisiongroup/alakazam';
5
+ import {
6
+ createAzureCodeInterpreterDownloadProcessor,
7
+ downloadGrantStore,
8
+ } from '@envisiongroup/alakazam/services/downloads';
9
+ import {
10
+ fileAttachmentProcessor,
11
+ uploadGrantStore,
12
+ uploadProviderRegistry,
13
+ } from '@envisiongroup/alakazam/services/uploads';
14
+ import { buildInstructions } from './prompts';
15
+
16
+ // Crea el cliente de Azure y selecciona el modelo LLM que se usara.
17
+ const azureClient = getAzureClient();
18
+
19
+ // Asegurate que el modelo especificado exista en tu proyecto de Azure Foundry
20
+ const llmModel = azureClient('gpt-5.4');
21
+
22
+ /**
23
+ * Agente de prueba general sobre gpt-5.4 mediante Azure OpenAI Responses API.
24
+ * Incluye tres capacidades que puedes reutilizar en otros agentes:
25
+ *
26
+ * - `code_interpreter` para procesar adjuntos de Office y generar archivos.
27
+ * - `fileAttachmentProcessor` para montar XLSX, DOCX, PPTX y CSV en el
28
+ * contenedor del sandbox.
29
+ * - `createAzureCodeInterpreterDownloadProcessor` para crear enlaces de
30
+ * descarga para los archivos generados.
31
+ *
32
+ * Úsalo para experimentar con prompts, adjuntos y respuestas del modelo. No
33
+ * está asociado a un caso de negocio específico.
34
+ */
35
+ export const azureOpenAISandboxAgent = new Agent({
36
+ id: 'azure-openai-sandbox-agent',
37
+ name: 'Sandbox Azure OpenAI',
38
+ description:
39
+ 'Agente sandbox para probar gpt-5.4 (via Azure OpenAI Responses API) con adjuntos y generacion de archivos. Acepta PDF / XLSX / DOCX / PPTX / CSV y puede generar archivos como entregable. Sin caso de uso especifico — para experimentar.',
40
+ instructions: () => buildInstructions(),
41
+ model: llmModel,
42
+ // Permite que la ruta envíe al cliente los fragmentos `reasoning-*`.
43
+ // Sin esta opción, el provider los filtra antes de enviarlos al cliente.
44
+ //
45
+ // Para que el reasoning realmente aparezca en el cliente hacen falta
46
+ // DOS cosas en paralelo:
47
+ // 1. `emitReasoning: true` en `hubAgent()`
48
+ // 2. `providerOptions.openai.reasoningSummary: 'auto'` (abajo) — pide
49
+ // al provider que genere el summary del reasoning.
50
+ // Si falta cualquiera de las dos, el reasoning no llega al cliente.
51
+ metadata: {
52
+ ...hubAgent({
53
+ catalog: { tags: ['sandbox', 'openai', 'azure'] },
54
+ access: { public: true },
55
+ capabilities: {
56
+ attachments: [
57
+ attachments.pdf,
58
+ attachments.images,
59
+ attachments.office,
60
+ ],
61
+ generatedFiles: true,
62
+ },
63
+ emitReasoning: true,
64
+ }),
65
+ },
66
+ defaultOptions: {
67
+ providerOptions: {
68
+ openai: {
69
+ // `reasoningEffort`: cuánto "piensa" el modelo internamente.
70
+ // `low` mantiene latencia baja para el sandbox.
71
+ reasoningEffort: 'low', // 'minimal' | 'low' | 'medium' | 'high'
72
+ // `reasoningSummary: 'auto'`: pide al provider que emita
73
+ // un resumen del razonamiento como chunks `reasoning-*`
74
+ // streamed. Combinado con `emitReasoning: true` en
75
+ // `hubAgent()` (que emite `metadata.emitReasoning`)
76
+ // y `sendReasoning: true` en el route, el cliente useChat
77
+ // los renderiza como sección "Razonando…" colapsable.
78
+ reasoningSummary: 'auto',
79
+ },
80
+ },
81
+ },
82
+ // `code_interpreter` inicial sin `container.fileIds`. Cuando el usuario
83
+ // adjunta XLSX, DOCX, PPTX o CSV, `fileAttachmentProcessor` guarda los
84
+ // identificadores en `requestContext` y reemplaza esta tool por otra que
85
+ // incluye el contenedor con esos archivos.
86
+ tools: {
87
+ code_interpreter: azureClient.tools.codeInterpreter(),
88
+ },
89
+ inputProcessors: [
90
+ fileAttachmentProcessor({
91
+ grantStore: uploadGrantStore,
92
+ providerRegistry: uploadProviderRegistry,
93
+ model: llmModel,
94
+ // Habilita la rama Office/CSV: cuando hay file_ids en el
95
+ // requestContext, el processor usa esta factory en
96
+ // processInputStep para reemplazar el code_interpreter del step
97
+ // por uno con container cargado.
98
+ codeInterpreterFactory: (args) =>
99
+ azureClient.tools.codeInterpreter(args),
100
+ }),
101
+ ],
102
+ outputProcessors: [
103
+ // Genera enlaces de descarga absolutos usando el origen de la petición.
104
+ // Funciona detrás de un proxy y tanto en Mastra Studio como en un SPA
105
+ // con un origen diferente.
106
+ createAzureCodeInterpreterDownloadProcessor({
107
+ grantStore: downloadGrantStore,
108
+ }),
109
+ ],
110
+ memory: new Memory(),
111
+ });
@@ -0,0 +1,42 @@
1
+ import {
2
+ codeInterpreterUsage,
3
+ composeInstructions,
4
+ immediateFeedback,
5
+ } from '@envisiongroup/alakazam/prompts/common/index.js';
6
+
7
+ const immediateFeedbackExamples = [
8
+ 'Recibí el archivo. Voy a revisarlo primero y luego usaré code_interpreter para los cálculos necesarios.',
9
+ 'Ya identifiqué los datos relevantes. Ahora voy a ejecutar el análisis en Python.',
10
+ 'Voy a generar el archivo solicitado y dejarlo listo para descarga.',
11
+ ];
12
+
13
+ const roleAndGoal = `Eres un asistente de propósito general construido sobre gpt-5.4 corriendo en Azure OpenAI.`;
14
+
15
+ const responseStyle = `# Como respondes
16
+
17
+ - En español, claro y conciso. Sin formalismos innecesarios.
18
+ - Pregunta directa → respuesta directa. No agregues estructura tipo "1, 2, 3" si no aporta.
19
+ - Si te piden algo que requiere mas analisis (resumir documento, comparar, extraer datos, hacer un calculo grande), si conviene estructurar — usa secciones / listas / tablas markdown segun ayude.`;
20
+
21
+ const limits = `# Limites
22
+
23
+ - No tienes acceso a internet ni a tools externas mas allá de \`code_interpreter\`.
24
+ - Si una pregunta requiere informacion que no tienes en el contexto ni en los adjuntos, dilo en vez de inventar.`;
25
+
26
+ export function buildInstructions() {
27
+ return composeInstructions([
28
+ roleAndGoal,
29
+ immediateFeedback({
30
+ toolName: 'code_interpreter',
31
+ examples: immediateFeedbackExamples,
32
+ }),
33
+ responseStyle,
34
+ codeInterpreterUsage({
35
+ toolName: 'code_interpreter',
36
+ additionalRules: [
37
+ 'Para una pregunta puntual con una cuenta corta (por ejemplo, 1 + 1), no es necesario abrir un sandbox.',
38
+ ],
39
+ }),
40
+ limits,
41
+ ]);
42
+ }
@@ -0,0 +1,93 @@
1
+ import type {
2
+ AuthenticatedUserContextValue,
3
+ EnrichedUserProfile,
4
+ } from '@envisiongroup/alakazam';
5
+
6
+ const responseStyle = `# Como respondes
7
+
8
+ - En español, claro y conciso.
9
+ - Pregunta directa → respuesta directa. No agregues estructura tipo "1, 2, 3" si no aporta.`;
10
+
11
+ const limits = `# Limites
12
+
13
+ - No tienes acceso a internet ni a tools externas más allá de las declaradas.
14
+ - Si una pregunta requiere información que no tienes en el contexto, dilo en vez de inventar.`;
15
+
16
+ /**
17
+ * Instrucciones del `user-context-agent`: cambian según haya (o no) un
18
+ * usuario autenticado en el `requestContext` del request. Es la demo del
19
+ * patrón "instrucciones dinámicas por contexto".
20
+ *
21
+ * - SIN usuario: el agente explica qué demuestra y que el usuario debe
22
+ * autenticarse (el hub corre detrás de auth; en Mastra Studio local no
23
+ * hay IdP, así que este camino es el esperado ahí).
24
+ * - CON usuario: el agente sabe quién es (oid/upn/nombre, roles, grupos) y
25
+ * — si el `UserProfileResolver` pudo enriquecerlo — su perfil (cargo,
26
+ * departamento, oficina). Sin config de Graph el resolver es Noop y el
27
+ * perfil llega `null`: el agente trabaja solo con la identidad del token.
28
+ */
29
+ export function buildUserContextInstructions(
30
+ authenticatedUser: AuthenticatedUserContextValue | undefined,
31
+ userProfile: EnrichedUserProfile | null,
32
+ ): string {
33
+ if (!authenticatedUser) {
34
+ return [
35
+ `Eres un agente de ejemplo que demuestra el uso del contexto de usuario autenticado del hub.`,
36
+ `# Estado actual
37
+
38
+ NO hay un usuario autenticado en el requestContext de este request (p. ej. estás corriendo en Mastra Studio local, donde no pasa por el middleware de auth del hub).
39
+
40
+ # Qué demuestras
41
+
42
+ - Cuando el request llega autenticado a través del hub, tu requestContext trae \`authenticatedUser\` (oid, upn, nombre, grupos del IdP y roles de aplicación resueltos) y estas instrucciones cambian para incluir esa identidad.
43
+ - Sin usuario autenticado no tienes tools disponibles (las tools se declaran condicionales por contexto). Explica esto si te piden acciones.`,
44
+ responseStyle,
45
+ limits,
46
+ ].join('\n\n');
47
+ }
48
+
49
+ const rolesLine = authenticatedUser.roles?.length
50
+ ? `- Roles de aplicación: ${authenticatedUser.roles.join(', ')}`
51
+ : '- Roles de aplicación: (ninguno resuelto)';
52
+
53
+ const profileSection = userProfile
54
+ ? `# Perfil enriquecido (UserProfileResolver)
55
+
56
+ ${[
57
+ userProfile.displayName
58
+ ? `- Nombre para mostrar: ${userProfile.displayName}`
59
+ : null,
60
+ userProfile.jobTitle ? `- Cargo: ${userProfile.jobTitle}` : null,
61
+ userProfile.department ? `- Departamento: ${userProfile.department}` : null,
62
+ userProfile.businessUnit
63
+ ? `- Gerencia/Unidad: ${userProfile.businessUnit}`
64
+ : null,
65
+ userProfile.officeLocation
66
+ ? `- Oficina: ${userProfile.officeLocation}`
67
+ : null,
68
+ ]
69
+ .filter(Boolean)
70
+ .join('\n')}`
71
+ : `# Perfil enriquecido
72
+
73
+ No disponible en este despliegue (sin config de Microsoft Graph el resolver es Noop → perfil null). Trabaja solo con la identidad del token.`;
74
+
75
+ return [
76
+ `Eres un agente de ejemplo que demuestra el uso del contexto de usuario autenticado del hub.`,
77
+ `# Usuario autenticado en este request
78
+
79
+ - oid: ${authenticatedUser.oid}
80
+ - UPN: ${authenticatedUser.upn}
81
+ ${authenticatedUser.name ? `- Nombre: ${authenticatedUser.name}` : ''}
82
+ ${rolesLine}
83
+ - Grupos del IdP: ${authenticatedUser.groups.length ? authenticatedUser.groups.join(', ') : '(ninguno)'}`,
84
+ profileSection,
85
+ `# Qué demuestras
86
+
87
+ - Sabes quién es el usuario sin preguntar: la identidad llegó en el requestContext vía el middleware de auth del hub.
88
+ - Tienes la tool \`get-my-profile\` disponible SOLO porque hay usuario autenticado (tools condicionales por contexto). Úsala cuando el usuario pregunte por sus datos, roles o grupos.
89
+ - Si el usuario pide algo fuera de su identidad/perfil, explica que eres un agente de ejemplo acotado a demostrar el contexto.`,
90
+ responseStyle,
91
+ limits,
92
+ ].join('\n\n');
93
+ }
@@ -0,0 +1,62 @@
1
+ import { createTool } from '@mastra/core/tools';
2
+ import { z } from 'zod';
3
+ import {
4
+ getAuthenticatedUserOrThrow,
5
+ getOptionalUserProfileFromRequestContext,
6
+ } from '@envisiongroup/alakazam';
7
+
8
+ /**
9
+ * Tool de ejemplo self-contained (sin llamadas externas): devuelve el
10
+ * usuario autenticado y su perfil enriquecido tal como quedaron en el
11
+ * `requestContext` del request.
12
+ *
13
+ * Demuestra el patrón de leer contexto dentro de `execute` via
14
+ * `context.requestContext` — el mismo que usan las tools de Microsoft
15
+ * Graph del hub real, pero sin necesitar credenciales de Graph.
16
+ */
17
+ export const getMyProfileTool = createTool({
18
+ id: 'get-my-profile',
19
+ description:
20
+ 'Devuelve la identidad del usuario autenticado (oid, upn, nombre, grupos, roles de aplicación) y, si está resuelto, su perfil enriquecido (cargo, departamento, oficina).',
21
+ inputSchema: z.object({}),
22
+ outputSchema: z.object({
23
+ oid: z.string(),
24
+ upn: z.string(),
25
+ name: z.string().optional(),
26
+ groups: z.array(z.string()),
27
+ roles: z.array(z.string()).optional(),
28
+ provider: z.string().optional(),
29
+ profile: z
30
+ .object({
31
+ displayName: z.string().optional(),
32
+ jobTitle: z.string().optional(),
33
+ department: z.string().optional(),
34
+ businessUnit: z.string().optional(),
35
+ officeLocation: z.string().optional(),
36
+ })
37
+ .nullable(),
38
+ }),
39
+ execute: async (_input, context) => {
40
+ const requestContext = context?.requestContext;
41
+ // `OrThrow` es seguro acá: el agente solo expone esta tool cuando
42
+ // YA verificó que hay usuario autenticado en el requestContext
43
+ // (ver `getUserContextTools` en user-context-agent.ts).
44
+ const authenticatedUser = getAuthenticatedUserOrThrow(requestContext);
45
+ const profile =
46
+ getOptionalUserProfileFromRequestContext(requestContext) ?? null;
47
+
48
+ return {
49
+ oid: authenticatedUser.oid,
50
+ upn: authenticatedUser.upn,
51
+ ...(authenticatedUser.name ? { name: authenticatedUser.name } : {}),
52
+ groups: authenticatedUser.groups,
53
+ ...(authenticatedUser.roles
54
+ ? { roles: authenticatedUser.roles }
55
+ : {}),
56
+ ...(authenticatedUser.provider
57
+ ? { provider: authenticatedUser.provider }
58
+ : {}),
59
+ profile,
60
+ };
61
+ },
62
+ });
@@ -0,0 +1,87 @@
1
+ import { Agent } from '@mastra/core/agent';
2
+ import type { ToolsInput } from '@mastra/core/agent';
3
+ import type { RequestContext } from '@mastra/core/request-context';
4
+ import { Memory } from '@mastra/memory';
5
+ import { getAzureClient } from '@envisiongroup/alakazam/services/azure-model';
6
+ import {
7
+ ensureUserProfileInRequestContext,
8
+ getAuthenticatedUserFromRequestContext,
9
+ getUserProfileResolver,
10
+ hubAgent,
11
+ optionalAuthenticatedUserRequestContextSchema,
12
+ } from '@envisiongroup/alakazam';
13
+ import { buildUserContextInstructions } from './prompts';
14
+ import { getMyProfileTool } from './tools/get-my-profile-tool';
15
+
16
+ // Crea el cliente de Azure y selecciona el modelo LLM que se usara.
17
+ const azureClient = getAzureClient();
18
+
19
+ // Asegurate que el modelo especificado exista en tu proyecto de Azure Foundry
20
+ const llmModel = azureClient('gpt-5.4');
21
+
22
+ // Obtiene el resolvedor encargado de cargar el perfil del usuario.
23
+ const userProfileResolver = getUserProfileResolver();
24
+
25
+ type RequestContextReader = Pick<RequestContext<any>, 'get'> | undefined;
26
+
27
+ /**
28
+ * Tools condicionales por contexto: sin usuario autenticado en el
29
+ * requestContext, el agente no expone ninguna tool. Es la demo del patrón
30
+ * `tools: ({ requestContext }) => ...` de Mastra.
31
+ */
32
+ function getUserContextTools(requestContext: RequestContextReader): ToolsInput {
33
+ if (!getAuthenticatedUserFromRequestContext(requestContext)) {
34
+ return {};
35
+ }
36
+
37
+ return { getMyProfileTool };
38
+ }
39
+
40
+ /**
41
+ * Agente de ejemplo: contexto del usuario autenticado.
42
+ *
43
+ * Versión self-contained del patrón que usa `personal-profile-agent` en el
44
+ * hub real, SIN dependencia de Microsoft Graph (funciona out-of-the-box en
45
+ * un proyecto recién scaffolded):
46
+ *
47
+ * - `requestContextSchema`: declara el shape del contexto que el agente
48
+ * espera (usuario opcional → corre también anónimo en Studio).
49
+ * - `instructions` dinámicas: con usuario, resuelve el perfil via
50
+ * `ensureUserProfileInRequestContext` (cachea el resultado en el propio
51
+ * requestContext) y compone instrucciones con la identidad.
52
+ * - `tools` condicionales: `get-my-profile` solo existe si hay usuario.
53
+ *
54
+ * Sin env vars de Graph, `getUserProfileResolver()` devuelve un resolver
55
+ * Noop → el perfil enriquecido llega `null` y el agente trabaja solo con
56
+ * la identidad del token (oid/upn/nombre/grupos/roles).
57
+ */
58
+ export const userContextAgent = new Agent({
59
+ id: 'user-context-agent',
60
+ name: 'Contexto de Usuario',
61
+ description:
62
+ 'Agente de ejemplo que demuestra el contexto del usuario autenticado: requestContextSchema, instrucciones dinámicas con la identidad del usuario y tools condicionales según haya (o no) usuario en el requestContext.',
63
+ requestContextSchema: optionalAuthenticatedUserRequestContextSchema,
64
+ instructions: async ({ requestContext }) => {
65
+ if (!getAuthenticatedUserFromRequestContext(requestContext)) {
66
+ return buildUserContextInstructions(undefined, null);
67
+ }
68
+
69
+ const userProfile = await ensureUserProfileInRequestContext(
70
+ requestContext,
71
+ userProfileResolver,
72
+ );
73
+ return buildUserContextInstructions(
74
+ getAuthenticatedUserFromRequestContext(requestContext),
75
+ userProfile,
76
+ );
77
+ },
78
+ model: llmModel,
79
+ tools: ({ requestContext }) => getUserContextTools(requestContext),
80
+ metadata: {
81
+ ...hubAgent({
82
+ catalog: { tags: ['ejemplo', 'usuario', 'contexto'] },
83
+ access: { public: true },
84
+ }),
85
+ },
86
+ memory: new Memory(),
87
+ }) as Agent;