@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.
- package/LICENSE +5 -0
- package/README.md +56 -0
- package/bin/create-alakazam.mjs +7 -0
- package/package.json +28 -0
- package/src/args.mjs +72 -0
- package/src/createAlakazamCli.mjs +115 -0
- package/src/projectName.mjs +33 -0
- package/src/prompts.mjs +63 -0
- package/src/registry.mjs +62 -0
- package/src/template.mjs +135 -0
- package/templates/default/.env.sample +55 -0
- package/templates/default/README.md +66 -0
- package/templates/default/gitignore +7 -0
- package/templates/default/package.json +36 -0
- package/templates/default/src/mastra/access/role-rules.ts +31 -0
- package/templates/default/src/mastra/agents/azure-claude-sandbox-agent/azure-claude-sandbox-agent.ts +108 -0
- package/templates/default/src/mastra/agents/azure-openai-sandbox-agent/azure-openai-sandbox-agent.ts +111 -0
- package/templates/default/src/mastra/agents/azure-openai-sandbox-agent/prompts/index.ts +42 -0
- package/templates/default/src/mastra/agents/user-context-agent/prompts/index.ts +93 -0
- package/templates/default/src/mastra/agents/user-context-agent/tools/get-my-profile-tool.ts +62 -0
- package/templates/default/src/mastra/agents/user-context-agent/user-context-agent.ts +87 -0
- package/templates/default/src/mastra/agents/weather-agent/weather-agent.ts +68 -0
- package/templates/default/src/mastra/index.ts +47 -0
- package/templates/default/src/mastra/scorers/weather-scorer.ts +92 -0
- package/templates/default/src/mastra/tools/weather-tool.ts +110 -0
- package/templates/default/src/mastra/workflows/weather-workflow.ts +198 -0
- package/templates/default/tsconfig.json +16 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Agent } from '@mastra/core/agent';
|
|
2
|
+
import { Memory } from '@mastra/memory';
|
|
3
|
+
import { hubAgent } from '@envisiongroup/alakazam';
|
|
4
|
+
import { weatherTool } from '../../tools/weather-tool';
|
|
5
|
+
import { scorers } from '../../scorers/weather-scorer';
|
|
6
|
+
import { getAzureClient } from '@envisiongroup/alakazam/services/azure-model';
|
|
7
|
+
|
|
8
|
+
// Crea el cliente de Azure y selecciona el modelo LLM que se usara.
|
|
9
|
+
const azureClient = getAzureClient();
|
|
10
|
+
|
|
11
|
+
// Asegurate que el modelo especificado exista en tu proyecto de Azure Foundry
|
|
12
|
+
const llmModel = azureClient('gpt-4.1-mini');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Agente de ejemplo que consulta el clima y puede sugerir actividades según
|
|
16
|
+
* las condiciones actuales. También muestra cómo asociar evaluadores a un
|
|
17
|
+
* agente para medir el uso de tools, la completitud y la traducción.
|
|
18
|
+
*/
|
|
19
|
+
export const weatherAgent = new Agent({
|
|
20
|
+
id: 'weather-agent',
|
|
21
|
+
metadata: {
|
|
22
|
+
...hubAgent({
|
|
23
|
+
catalog: { tags: ['weather', 'demo'] },
|
|
24
|
+
access: { public: true },
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
name: 'Weather Agent',
|
|
28
|
+
instructions: `
|
|
29
|
+
You are a helpful weather assistant that provides accurate weather information and can help planning activities based on the weather.
|
|
30
|
+
|
|
31
|
+
Your primary function is to help users get weather details for specific locations. When responding:
|
|
32
|
+
- Always ask for a location if none is provided
|
|
33
|
+
- If the location name isn't in English, please translate it
|
|
34
|
+
- If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
|
|
35
|
+
- Include relevant details like humidity, wind conditions, and precipitation
|
|
36
|
+
- Keep responses concise but informative
|
|
37
|
+
- If the user asks for activities and provides the weather forecast, suggest activities based on the weather forecast.
|
|
38
|
+
- If the user asks for activities, respond in the format they request.
|
|
39
|
+
|
|
40
|
+
Use the weatherTool to fetch current weather data.
|
|
41
|
+
`,
|
|
42
|
+
model: llmModel,
|
|
43
|
+
tools: { weatherTool },
|
|
44
|
+
scorers: {
|
|
45
|
+
toolCallAppropriateness: {
|
|
46
|
+
scorer: scorers.toolCallAppropriatenessScorer,
|
|
47
|
+
sampling: {
|
|
48
|
+
type: 'ratio',
|
|
49
|
+
rate: 1,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
completeness: {
|
|
53
|
+
scorer: scorers.completenessScorer,
|
|
54
|
+
sampling: {
|
|
55
|
+
type: 'ratio',
|
|
56
|
+
rate: 1,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
translation: {
|
|
60
|
+
scorer: scorers.translationScorer,
|
|
61
|
+
sampling: {
|
|
62
|
+
type: 'ratio',
|
|
63
|
+
rate: 1,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
memory: new Memory(),
|
|
68
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAgentsHub,
|
|
3
|
+
entraIdentityProvider,
|
|
4
|
+
staticRoleResolver,
|
|
5
|
+
} from '@envisiongroup/alakazam';
|
|
6
|
+
|
|
7
|
+
// --- Dominio de ESTE proyecto ------------------------------------------------
|
|
8
|
+
import { weatherAgent } from './agents/weather-agent/weather-agent';
|
|
9
|
+
import { azureOpenAISandboxAgent } from './agents/azure-openai-sandbox-agent/azure-openai-sandbox-agent'; // __EXAMPLES__
|
|
10
|
+
import { azureClaudeSandboxAgent } from './agents/azure-claude-sandbox-agent/azure-claude-sandbox-agent'; // __EXAMPLES__
|
|
11
|
+
import { userContextAgent } from './agents/user-context-agent/user-context-agent'; // __EXAMPLES__
|
|
12
|
+
import { weatherTool } from './tools/weather-tool';
|
|
13
|
+
import { weatherWorkflow } from './workflows/weather-workflow';
|
|
14
|
+
import {
|
|
15
|
+
toolCallAppropriatenessScorer,
|
|
16
|
+
completenessScorer,
|
|
17
|
+
translationScorer,
|
|
18
|
+
} from './scorers/weather-scorer';
|
|
19
|
+
import { ROLE_RULES } from './access/role-rules';
|
|
20
|
+
|
|
21
|
+
// --- Hub transversal (@envisiongroup/alakazam) -------------------------------
|
|
22
|
+
// Todo el wiring (storage, observability, middlewares de auth/origin, CORS y
|
|
23
|
+
// rutas custom) lo resuelve `createAgentsHub`. Acá solo declaras tu dominio:
|
|
24
|
+
// agents, tools, workflows y scorers propios.
|
|
25
|
+
//
|
|
26
|
+
// El catálogo y el acceso de cada recurso se declaran en su propia
|
|
27
|
+
// `metadata.envisionHub` (vía `hubAgent()` / `hubWorkflow()`). Un recurso
|
|
28
|
+
// registrado sin declaración de acceso falla el arranque (fail-fast).
|
|
29
|
+
export const mastra = await createAgentsHub({
|
|
30
|
+
agents: {
|
|
31
|
+
weatherAgent,
|
|
32
|
+
azureOpenAISandboxAgent, // __EXAMPLES__
|
|
33
|
+
azureClaudeSandboxAgent, // __EXAMPLES__
|
|
34
|
+
userContextAgent, // __EXAMPLES__
|
|
35
|
+
},
|
|
36
|
+
tools: { weatherTool },
|
|
37
|
+
workflows: { weatherWorkflow },
|
|
38
|
+
scorers: {
|
|
39
|
+
toolCallAppropriatenessScorer,
|
|
40
|
+
completenessScorer,
|
|
41
|
+
translationScorer,
|
|
42
|
+
},
|
|
43
|
+
auth: {
|
|
44
|
+
providers: [entraIdentityProvider()],
|
|
45
|
+
roleResolver: staticRoleResolver(ROLE_RULES),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createToolCallAccuracyScorerCode } from '@mastra/evals/scorers/prebuilt';
|
|
3
|
+
import { createCompletenessScorer } from '@mastra/evals/scorers/prebuilt';
|
|
4
|
+
import {
|
|
5
|
+
getAssistantMessageFromRunOutput,
|
|
6
|
+
getUserMessageFromRunInput,
|
|
7
|
+
} from '@mastra/evals/scorers/utils';
|
|
8
|
+
import { createScorer } from '@mastra/core/evals';
|
|
9
|
+
|
|
10
|
+
/** Comprueba si el agente eligió la tool de clima esperada. */
|
|
11
|
+
export const toolCallAppropriatenessScorer = createToolCallAccuracyScorerCode({
|
|
12
|
+
expectedTool: 'weatherTool',
|
|
13
|
+
strictMode: false,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Comprueba si la respuesta contiene la información necesaria. */
|
|
17
|
+
export const completenessScorer = createCompletenessScorer();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Evalúa con otro modelo si una ubicación en otro idioma se tradujo bien.
|
|
21
|
+
* Si la ubicación ya está en inglés, la evaluación otorga la puntuación máxima.
|
|
22
|
+
*/
|
|
23
|
+
export const translationScorer = createScorer({
|
|
24
|
+
id: 'translation-quality-scorer',
|
|
25
|
+
name: 'Translation Quality',
|
|
26
|
+
description:
|
|
27
|
+
'Checks that non-English location names are translated and used correctly',
|
|
28
|
+
type: 'agent',
|
|
29
|
+
judge: {
|
|
30
|
+
model: 'openai/gpt-5-mini',
|
|
31
|
+
instructions:
|
|
32
|
+
'You are an expert evaluator of translation quality for geographic locations. ' +
|
|
33
|
+
'Determine whether the user text mentions a non-English location and whether the assistant correctly uses an English translation of that location. ' +
|
|
34
|
+
'Be lenient with transliteration differences and diacritics. ' +
|
|
35
|
+
'Return only the structured JSON matching the provided schema.',
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
.preprocess(({ run }) => {
|
|
39
|
+
const userText = getUserMessageFromRunInput(run.input) || '';
|
|
40
|
+
const assistantText =
|
|
41
|
+
getAssistantMessageFromRunOutput(run.output) || '';
|
|
42
|
+
return { userText, assistantText };
|
|
43
|
+
})
|
|
44
|
+
.analyze({
|
|
45
|
+
description:
|
|
46
|
+
'Extract location names and detect language/translation adequacy',
|
|
47
|
+
outputSchema: z.object({
|
|
48
|
+
nonEnglish: z.boolean(),
|
|
49
|
+
translated: z.boolean(),
|
|
50
|
+
confidence: z.number().min(0).max(1).default(1),
|
|
51
|
+
explanation: z.string().default(''),
|
|
52
|
+
}),
|
|
53
|
+
createPrompt: ({ results }) => `
|
|
54
|
+
You are evaluating if a weather assistant correctly handled translation of a non-English location.
|
|
55
|
+
User text:
|
|
56
|
+
"""
|
|
57
|
+
${results.preprocessStepResult.userText}
|
|
58
|
+
"""
|
|
59
|
+
Assistant response:
|
|
60
|
+
"""
|
|
61
|
+
${results.preprocessStepResult.assistantText}
|
|
62
|
+
"""
|
|
63
|
+
Tasks:
|
|
64
|
+
1) Identify if the user mentioned a location that appears non-English.
|
|
65
|
+
2) If non-English, check whether the assistant used a correct English translation of that location in its response.
|
|
66
|
+
3) Be lenient with transliteration differences (e.g., accents/diacritics).
|
|
67
|
+
Return JSON with fields:
|
|
68
|
+
{
|
|
69
|
+
"nonEnglish": boolean,
|
|
70
|
+
"translated": boolean,
|
|
71
|
+
"confidence": number,
|
|
72
|
+
"explanation": string
|
|
73
|
+
}
|
|
74
|
+
`,
|
|
75
|
+
})
|
|
76
|
+
.generateScore(({ results }) => {
|
|
77
|
+
const r = (results as any)?.analyzeStepResult || {};
|
|
78
|
+
if (!r.nonEnglish) return 1;
|
|
79
|
+
if (r.translated)
|
|
80
|
+
return Math.max(0, Math.min(1, 0.7 + 0.3 * (r.confidence ?? 1)));
|
|
81
|
+
return 0;
|
|
82
|
+
})
|
|
83
|
+
.generateReason(({ results, score }) => {
|
|
84
|
+
const r = (results as any)?.analyzeStepResult || {};
|
|
85
|
+
return `Translation scoring: nonEnglish=${r.nonEnglish ?? false}, translated=${r.translated ?? false}, confidence=${r.confidence ?? 0}. Score=${score}. ${r.explanation ?? ''}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const scorers = {
|
|
89
|
+
toolCallAppropriatenessScorer,
|
|
90
|
+
completenessScorer,
|
|
91
|
+
translationScorer,
|
|
92
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createTool } from '@mastra/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface GeocodingResponse {
|
|
5
|
+
results: {
|
|
6
|
+
latitude: number;
|
|
7
|
+
longitude: number;
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
interface WeatherResponse {
|
|
12
|
+
current: {
|
|
13
|
+
time: string;
|
|
14
|
+
temperature_2m: number;
|
|
15
|
+
apparent_temperature: number;
|
|
16
|
+
relative_humidity_2m: number;
|
|
17
|
+
wind_speed_10m: number;
|
|
18
|
+
wind_gusts_10m: number;
|
|
19
|
+
weather_code: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Consulta el clima actual de una ubicación usando Open-Meteo.
|
|
25
|
+
*
|
|
26
|
+
* Primero convierte el nombre de la ciudad en coordenadas y después obtiene
|
|
27
|
+
* la temperatura, la humedad, el viento y las condiciones actuales.
|
|
28
|
+
*/
|
|
29
|
+
export const weatherTool = createTool({
|
|
30
|
+
id: 'get-weather',
|
|
31
|
+
description: 'Get current weather for a location',
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
location: z.string().describe('City name'),
|
|
34
|
+
}),
|
|
35
|
+
outputSchema: z.object({
|
|
36
|
+
temperature: z.number(),
|
|
37
|
+
feelsLike: z.number(),
|
|
38
|
+
humidity: z.number(),
|
|
39
|
+
windSpeed: z.number(),
|
|
40
|
+
windGust: z.number(),
|
|
41
|
+
conditions: z.string(),
|
|
42
|
+
location: z.string(),
|
|
43
|
+
}),
|
|
44
|
+
execute: async (inputData) => {
|
|
45
|
+
return await getWeather(inputData.location);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Obtiene los datos del clima después de resolver la ubicación. */
|
|
50
|
+
const getWeather = async (location: string) => {
|
|
51
|
+
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
|
|
52
|
+
const geocodingResponse = await fetch(geocodingUrl);
|
|
53
|
+
const geocodingData = (await geocodingResponse.json()) as GeocodingResponse;
|
|
54
|
+
|
|
55
|
+
if (!geocodingData.results?.[0]) {
|
|
56
|
+
throw new Error(`Location '${location}' not found`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { latitude, longitude, name } = geocodingData.results[0];
|
|
60
|
+
|
|
61
|
+
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
|
|
62
|
+
|
|
63
|
+
const response = await fetch(weatherUrl);
|
|
64
|
+
const data = (await response.json()) as WeatherResponse;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
temperature: data.current.temperature_2m,
|
|
68
|
+
feelsLike: data.current.apparent_temperature,
|
|
69
|
+
humidity: data.current.relative_humidity_2m,
|
|
70
|
+
windSpeed: data.current.wind_speed_10m,
|
|
71
|
+
windGust: data.current.wind_gusts_10m,
|
|
72
|
+
conditions: getWeatherCondition(data.current.weather_code),
|
|
73
|
+
location: name,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Convierte el código meteorológico de Open-Meteo en texto legible. */
|
|
78
|
+
function getWeatherCondition(code: number): string {
|
|
79
|
+
const conditions: Record<number, string> = {
|
|
80
|
+
0: 'Clear sky',
|
|
81
|
+
1: 'Mainly clear',
|
|
82
|
+
2: 'Partly cloudy',
|
|
83
|
+
3: 'Overcast',
|
|
84
|
+
45: 'Foggy',
|
|
85
|
+
48: 'Depositing rime fog',
|
|
86
|
+
51: 'Light drizzle',
|
|
87
|
+
53: 'Moderate drizzle',
|
|
88
|
+
55: 'Dense drizzle',
|
|
89
|
+
56: 'Light freezing drizzle',
|
|
90
|
+
57: 'Dense freezing drizzle',
|
|
91
|
+
61: 'Slight rain',
|
|
92
|
+
63: 'Moderate rain',
|
|
93
|
+
65: 'Heavy rain',
|
|
94
|
+
66: 'Light freezing rain',
|
|
95
|
+
67: 'Heavy freezing rain',
|
|
96
|
+
71: 'Slight snow fall',
|
|
97
|
+
73: 'Moderate snow fall',
|
|
98
|
+
75: 'Heavy snow fall',
|
|
99
|
+
77: 'Snow grains',
|
|
100
|
+
80: 'Slight rain showers',
|
|
101
|
+
81: 'Moderate rain showers',
|
|
102
|
+
82: 'Violent rain showers',
|
|
103
|
+
85: 'Slight snow showers',
|
|
104
|
+
86: 'Heavy snow showers',
|
|
105
|
+
95: 'Thunderstorm',
|
|
106
|
+
96: 'Thunderstorm with slight hail',
|
|
107
|
+
99: 'Thunderstorm with heavy hail',
|
|
108
|
+
};
|
|
109
|
+
return conditions[code] || 'Unknown';
|
|
110
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createStep, createWorkflow } from '@mastra/core/workflows';
|
|
2
|
+
import { hubWorkflow } from '@envisiongroup/alakazam';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/** Datos que se comparten entre los pasos del workflow. */
|
|
6
|
+
const forecastSchema = z.object({
|
|
7
|
+
date: z.string(),
|
|
8
|
+
maxTemp: z.number(),
|
|
9
|
+
minTemp: z.number(),
|
|
10
|
+
precipitationChance: z.number(),
|
|
11
|
+
condition: z.string(),
|
|
12
|
+
location: z.string(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Convierte un código de Open-Meteo en una condición legible. */
|
|
16
|
+
function getWeatherCondition(code: number): string {
|
|
17
|
+
const conditions: Record<number, string> = {
|
|
18
|
+
0: 'Clear sky',
|
|
19
|
+
1: 'Mainly clear',
|
|
20
|
+
2: 'Partly cloudy',
|
|
21
|
+
3: 'Overcast',
|
|
22
|
+
45: 'Foggy',
|
|
23
|
+
48: 'Depositing rime fog',
|
|
24
|
+
51: 'Light drizzle',
|
|
25
|
+
53: 'Moderate drizzle',
|
|
26
|
+
55: 'Dense drizzle',
|
|
27
|
+
61: 'Slight rain',
|
|
28
|
+
63: 'Moderate rain',
|
|
29
|
+
65: 'Heavy rain',
|
|
30
|
+
71: 'Slight snow fall',
|
|
31
|
+
73: 'Moderate snow fall',
|
|
32
|
+
75: 'Heavy snow fall',
|
|
33
|
+
95: 'Thunderstorm',
|
|
34
|
+
};
|
|
35
|
+
return conditions[code] || 'Unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Busca la ubicación y obtiene el pronóstico del clima. */
|
|
39
|
+
const fetchWeather = createStep({
|
|
40
|
+
id: 'fetch-weather',
|
|
41
|
+
description: 'Fetches weather forecast for a given city',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
city: z.string().describe('The city to get the weather for'),
|
|
44
|
+
}),
|
|
45
|
+
outputSchema: forecastSchema,
|
|
46
|
+
execute: async ({ inputData }) => {
|
|
47
|
+
if (!inputData) {
|
|
48
|
+
throw new Error('Input data not found');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(inputData.city)}&count=1`;
|
|
52
|
+
const geocodingResponse = await fetch(geocodingUrl);
|
|
53
|
+
const geocodingData = (await geocodingResponse.json()) as {
|
|
54
|
+
results: { latitude: number; longitude: number; name: string }[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (!geocodingData.results?.[0]) {
|
|
58
|
+
throw new Error(`Location '${inputData.city}' not found`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { latitude, longitude, name } = geocodingData.results[0];
|
|
62
|
+
|
|
63
|
+
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=precipitation,weathercode&timezone=auto,&hourly=precipitation_probability,temperature_2m`;
|
|
64
|
+
const response = await fetch(weatherUrl);
|
|
65
|
+
const data = (await response.json()) as {
|
|
66
|
+
current: {
|
|
67
|
+
time: string;
|
|
68
|
+
precipitation: number;
|
|
69
|
+
weathercode: number;
|
|
70
|
+
};
|
|
71
|
+
hourly: {
|
|
72
|
+
precipitation_probability: number[];
|
|
73
|
+
temperature_2m: number[];
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const forecast = {
|
|
78
|
+
date: new Date().toISOString(),
|
|
79
|
+
maxTemp: Math.max(...data.hourly.temperature_2m),
|
|
80
|
+
minTemp: Math.min(...data.hourly.temperature_2m),
|
|
81
|
+
condition: getWeatherCondition(data.current.weathercode),
|
|
82
|
+
precipitationChance: data.hourly.precipitation_probability.reduce(
|
|
83
|
+
(acc, curr) => Math.max(acc, curr),
|
|
84
|
+
0,
|
|
85
|
+
),
|
|
86
|
+
location: name,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return forecast;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** Usa el agente del clima para proponer actividades adecuadas. */
|
|
94
|
+
const planActivities = createStep({
|
|
95
|
+
id: 'plan-activities',
|
|
96
|
+
description: 'Suggests activities based on weather conditions',
|
|
97
|
+
inputSchema: forecastSchema,
|
|
98
|
+
outputSchema: z.object({
|
|
99
|
+
activities: z.string(),
|
|
100
|
+
}),
|
|
101
|
+
execute: async ({ inputData, mastra }) => {
|
|
102
|
+
const forecast = inputData;
|
|
103
|
+
|
|
104
|
+
if (!forecast) {
|
|
105
|
+
throw new Error('Forecast data not found');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const agent = mastra?.getAgent('weatherAgent');
|
|
109
|
+
if (!agent) {
|
|
110
|
+
throw new Error('Weather agent not found');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const prompt = `Based on the following weather forecast for ${forecast.location}, suggest appropriate activities:
|
|
114
|
+
${JSON.stringify(forecast, null, 2)}
|
|
115
|
+
For each day in the forecast, structure your response exactly as follows:
|
|
116
|
+
|
|
117
|
+
📅 [Day, Month Date, Year]
|
|
118
|
+
═══════════════════════════
|
|
119
|
+
|
|
120
|
+
🌡️ WEATHER SUMMARY
|
|
121
|
+
• Conditions: [brief description]
|
|
122
|
+
• Temperature: [X°C/Y°F to A°C/B°F]
|
|
123
|
+
• Precipitation: [X% chance]
|
|
124
|
+
|
|
125
|
+
🌅 MORNING ACTIVITIES
|
|
126
|
+
Outdoor:
|
|
127
|
+
• [Activity Name] - [Brief description including specific location/route]
|
|
128
|
+
Best timing: [specific time range]
|
|
129
|
+
Note: [relevant weather consideration]
|
|
130
|
+
|
|
131
|
+
🌞 AFTERNOON ACTIVITIES
|
|
132
|
+
Outdoor:
|
|
133
|
+
• [Activity Name] - [Brief description including specific location/route]
|
|
134
|
+
Best timing: [specific time range]
|
|
135
|
+
Note: [relevant weather consideration]
|
|
136
|
+
|
|
137
|
+
🏠 INDOOR ALTERNATIVES
|
|
138
|
+
• [Activity Name] - [Brief description including specific venue]
|
|
139
|
+
Ideal for: [weather condition that would trigger this alternative]
|
|
140
|
+
|
|
141
|
+
⚠️ SPECIAL CONSIDERATIONS
|
|
142
|
+
• [Any relevant weather warnings, UV index, wind conditions, etc.]
|
|
143
|
+
|
|
144
|
+
Guidelines:
|
|
145
|
+
- Suggest 2-3 time-specific outdoor activities per day
|
|
146
|
+
- Include 1-2 indoor backup options
|
|
147
|
+
- For precipitation >50%, lead with indoor activities
|
|
148
|
+
- All activities must be specific to the location
|
|
149
|
+
- Include specific venues, trails, or locations
|
|
150
|
+
- Consider activity intensity based on temperature
|
|
151
|
+
- Keep descriptions concise but informative
|
|
152
|
+
|
|
153
|
+
Maintain this exact formatting for consistency, using the emoji and section headers as shown.`;
|
|
154
|
+
|
|
155
|
+
const response = await agent.stream([
|
|
156
|
+
{
|
|
157
|
+
role: 'user',
|
|
158
|
+
content: prompt,
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
let activitiesText = '';
|
|
163
|
+
|
|
164
|
+
for await (const chunk of response.textStream) {
|
|
165
|
+
process.stdout.write(chunk);
|
|
166
|
+
activitiesText += chunk;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
activities: activitiesText,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Workflow de ejemplo con dos pasos: obtiene el clima y propone actividades.
|
|
177
|
+
*/
|
|
178
|
+
const weatherWorkflow = createWorkflow({
|
|
179
|
+
id: 'weather-workflow',
|
|
180
|
+
metadata: {
|
|
181
|
+
...hubWorkflow({
|
|
182
|
+
catalog: { tags: ['weather', 'demo'] },
|
|
183
|
+
access: { public: true },
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
inputSchema: z.object({
|
|
187
|
+
city: z.string().describe('The city to get the weather for'),
|
|
188
|
+
}),
|
|
189
|
+
outputSchema: z.object({
|
|
190
|
+
activities: z.string(),
|
|
191
|
+
}),
|
|
192
|
+
})
|
|
193
|
+
.then(fetchWeather)
|
|
194
|
+
.then(planActivities);
|
|
195
|
+
|
|
196
|
+
weatherWorkflow.commit();
|
|
197
|
+
|
|
198
|
+
export { weatherWorkflow };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"types": ["node"],
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"outDir": "dist"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|