@hazeljs/cli 0.4.1 → 0.5.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.
@@ -0,0 +1,170 @@
1
+ import { Controller, Post, Body, Service, Res } from '@hazeljs/core';
2
+ import type { HazelResponse } from '@hazeljs/core';
3
+ import { Agent, Tool, AgentService } from '@hazeljs/agent';
4
+
5
+ // AI Agent with tools — 3 tools: getWeather, getForecast, convertTemperature
6
+ // The agent autonomously picks the right tool(s) at runtime based on the request.
7
+ @Agent({
8
+ name: 'WeatherAgent',
9
+ description: 'AI assistant that can provide weather information',
10
+ systemPrompt: 'You are a helpful assistant that can provide weather information for any city.',
11
+ })
12
+ @Service()
13
+ export class WeatherAgent {
14
+ constructor(private readonly agentService: AgentService) {}
15
+
16
+ @Tool({
17
+ description: 'Get current weather conditions for a city',
18
+ parameters: [
19
+ {
20
+ name: 'city',
21
+ type: 'string',
22
+ description: 'The city to get weather for',
23
+ required: true,
24
+ },
25
+ ],
26
+ })
27
+ async getWeather(input: { city: string }) {
28
+ // In real app, call a real weather API
29
+ return {
30
+ city: input.city,
31
+ temperature: '72°F',
32
+ condition: 'sunny',
33
+ humidity: '45%',
34
+ windSpeed: '10 mph',
35
+ };
36
+ }
37
+
38
+ @Tool({
39
+ description: 'Get a multi-day weather forecast for a city',
40
+ parameters: [
41
+ {
42
+ name: 'city',
43
+ type: 'string',
44
+ description: 'The city to get forecast for',
45
+ required: true,
46
+ },
47
+ {
48
+ name: 'days',
49
+ type: 'number',
50
+ description: 'Number of days to forecast (1-7)',
51
+ required: true,
52
+ },
53
+ ],
54
+ })
55
+ async getForecast(input: { city: string; days: number }) {
56
+ // In real app, call a real forecast API
57
+ const forecast = Array.from({ length: input.days }, (_, i) => ({
58
+ day: i + 1,
59
+ condition: ['sunny', 'cloudy', 'rainy', 'sunny', 'partly cloudy', 'sunny', 'windy'][i % 7],
60
+ high: `${68 + i}°F`,
61
+ low: `${52 + i}°F`,
62
+ }));
63
+ return { city: input.city, days: input.days, forecast };
64
+ }
65
+
66
+ @Tool({
67
+ description: 'Convert a temperature value between Celsius, Fahrenheit, and Kelvin',
68
+ parameters: [
69
+ {
70
+ name: 'value',
71
+ type: 'number',
72
+ description: 'The temperature value to convert',
73
+ required: true,
74
+ },
75
+ {
76
+ name: 'fromUnit',
77
+ type: 'string',
78
+ description: 'The unit to convert from: celsius, fahrenheit, or kelvin',
79
+ required: true,
80
+ },
81
+ {
82
+ name: 'toUnit',
83
+ type: 'string',
84
+ description: 'The unit to convert to: celsius, fahrenheit, or kelvin',
85
+ required: true,
86
+ },
87
+ ],
88
+ })
89
+ async convertTemperature(input: { value: number; fromUnit: string; toUnit: string }) {
90
+ const { value, fromUnit, toUnit } = input;
91
+ let celsius: number;
92
+
93
+ switch (fromUnit.toLowerCase()) {
94
+ case 'fahrenheit': celsius = (value - 32) * (5 / 9); break;
95
+ case 'kelvin': celsius = value - 273.15; break;
96
+ default: celsius = value;
97
+ }
98
+
99
+ let result: number;
100
+ switch (toUnit.toLowerCase()) {
101
+ case 'fahrenheit': result = celsius * (9 / 5) + 32; break;
102
+ case 'kelvin': result = celsius + 273.15; break;
103
+ default: result = celsius;
104
+ }
105
+
106
+ return {
107
+ original: `${value}°${fromUnit.charAt(0).toUpperCase()}`,
108
+ converted: `${Math.round(result * 10) / 10}°${toUnit.charAt(0).toUpperCase()}`,
109
+ };
110
+ }
111
+
112
+ async execute(input: string) {
113
+ const result = await this.agentService.execute('WeatherAgent', input);
114
+ return result.response;
115
+ }
116
+
117
+ // Returns the full reasoning trace: every step the agent took, which tool
118
+ // was called, what the input/output was, and how long each step took.
119
+ async executeWithTrace(input: string) {
120
+ const result = await this.agentService.execute('WeatherAgent', input);
121
+ const steps = result.steps.map((step) => ({
122
+ step: step.stepNumber,
123
+ state: step.state,
124
+ tool: step.action?.toolName ?? null,
125
+ toolInput: step.action?.toolInput ?? null,
126
+ toolOutput: step.result?.output ?? null,
127
+ duration: step.duration ?? null,
128
+ }));
129
+ return { response: result.response, steps, totalDuration: result.duration };
130
+ }
131
+ }
132
+
133
+ @Controller('agent')
134
+ export class AgentController {
135
+ constructor(
136
+ private readonly agent: WeatherAgent,
137
+ private readonly agentService: AgentService,
138
+ ) {}
139
+
140
+ @Post()
141
+ async askAgent(@Body() body: { message: string }) {
142
+ const response = await this.agent.execute(body.message);
143
+ return { response };
144
+ }
145
+
146
+ // Feature: Execution trace — see every reasoning step the agent took
147
+ @Post('trace')
148
+ async traceAgent(@Body() body: { message: string }) {
149
+ return await this.agent.executeWithTrace(body.message);
150
+ }
151
+
152
+ // Feature: Streaming — token-by-token SSE stream of the agent's response
153
+ @Post('stream')
154
+ async streamAgent(@Body() body: { message: string }, @Res() res: HazelResponse) {
155
+ res.setHeader('Content-Type', 'text/event-stream');
156
+ res.setHeader('Cache-Control', 'no-cache');
157
+ res.setHeader('Connection', 'keep-alive');
158
+
159
+ for await (const chunk of this.agentService.executeStream('WeatherAgent', body.message)) {
160
+ if (chunk.type === 'token') {
161
+ res.write(`data: ${JSON.stringify({ type: 'token', content: chunk.content })}\n\n`);
162
+ } else if (chunk.type === 'step') {
163
+ res.write(`data: ${JSON.stringify({ type: 'step', state: chunk.step.state, tool: chunk.step.action?.toolName ?? null })}\n\n`);
164
+ } else if (chunk.type === 'done') {
165
+ res.write(`data: ${JSON.stringify({ type: 'done', response: chunk.result.response })}\n\n`);
166
+ }
167
+ }
168
+ res.end();
169
+ }
170
+ }
@@ -0,0 +1,124 @@
1
+ import { Controller, Post, Body, Service } from '@hazeljs/core';
2
+ import { Agent, Tool, Delegate, AgentService } from '@hazeljs/agent';
3
+
4
+ // ─── FactsAgent ───────────────────────────────────────────────────────────────
5
+ // A standalone specialist agent the Supervisor can route to.
6
+ // It knows fun facts and travel tips about cities.
7
+ @Agent({
8
+ name: 'FactsAgent',
9
+ description: 'Provides fun facts, cultural highlights, and travel tips about cities',
10
+ systemPrompt: 'You are a knowledgeable travel guide. Use your tools to provide interesting facts and tips about cities.',
11
+ })
12
+ @Service()
13
+ export class FactsAgent {
14
+ @Tool({
15
+ description: 'Get fun facts, cultural highlights, and travel tips for a city',
16
+ parameters: [
17
+ {
18
+ name: 'city',
19
+ type: 'string',
20
+ description: 'The city to get facts and travel tips for',
21
+ required: true,
22
+ },
23
+ ],
24
+ })
25
+ async getCityFacts(input: { city: string }) {
26
+ // In real app, call a travel/facts API or use a knowledge base
27
+ const facts: Record<string, { fact: string; tip: string; bestTime: string }> = {
28
+ paris: { fact: 'Paris has more than 470 parks and gardens.', tip: 'Buy a Paris Museum Pass to skip long queues.', bestTime: 'April–June or September–November' },
29
+ tokyo: { fact: 'Tokyo has the world\'s busiest pedestrian crossing at Shibuya.', tip: 'Get a Suica card for seamless transit across the city.', bestTime: 'March–May (cherry blossom) or October–November' },
30
+ london: { fact: 'London has over 170 museums, most of which are free.', tip: 'The Oyster card gives the cheapest fares on public transport.', bestTime: 'May–September for mild weather' },
31
+ newyork: { fact: 'New York City has 468 subway stations — the most in the world.', tip: 'Walk the High Line for great views and free art installations.', bestTime: 'April–June or September–November' },
32
+ };
33
+
34
+ const key = input.city.toLowerCase().replace(/\s+/g, '');
35
+ const data = facts[key] ?? {
36
+ fact: `${input.city} is a fascinating city with a rich history and culture.`,
37
+ tip: `Explore local neighborhoods and try the street food in ${input.city}.`,
38
+ bestTime: 'Spring and autumn are generally pleasant for travel.',
39
+ };
40
+
41
+ return { city: input.city, ...data };
42
+ }
43
+ }
44
+
45
+ // ─── TravelAgent ──────────────────────────────────────────────────────────────
46
+ // An orchestrator agent that delegates to WeatherAgent via @Delegate.
47
+ // The LLM sees each @Delegate method as a tool — agent-to-agent calls are
48
+ // completely transparent to the model.
49
+ @Agent({
50
+ name: 'TravelAgent',
51
+ description: 'Travel planning assistant that checks weather and provides itinerary advice',
52
+ systemPrompt: 'You are a travel planner. Use your tools to check weather and forecasts for cities, then provide helpful travel advice based on the conditions.',
53
+ })
54
+ @Service()
55
+ export class TravelAgent {
56
+ constructor(private readonly agentService: AgentService) {}
57
+
58
+ // @Delegate transparently routes this tool call to WeatherAgent at runtime.
59
+ // The LLM sees it as a regular tool — agent-to-agent is completely transparent.
60
+ @Delegate({
61
+ agent: 'WeatherAgent',
62
+ description: 'Check weather conditions and forecasts for a city to inform travel recommendations',
63
+ inputField: 'input',
64
+ })
65
+ async checkWeather(input: string): Promise<string> {
66
+ return ''; // body replaced at runtime by AgentRuntime
67
+ }
68
+
69
+ // @Delegate to a different specialist agent — FactsAgent
70
+ @Delegate({
71
+ agent: 'FactsAgent',
72
+ description: 'Get travel tips, cultural highlights, and fun facts about a city',
73
+ inputField: 'input',
74
+ })
75
+ async checkFacts(input: string): Promise<string> {
76
+ return ''; // body replaced at runtime by AgentRuntime
77
+ }
78
+
79
+ async execute(input: string) {
80
+ const result = await this.agentService.execute('TravelAgent', input);
81
+ return result.response;
82
+ }
83
+ }
84
+
85
+ // ─── TravelController ─────────────────────────────────────────────────────────
86
+ @Controller('travel')
87
+ export class TravelController {
88
+ constructor(
89
+ private readonly travelAgent: TravelAgent,
90
+ private readonly agentService: AgentService,
91
+ ) {}
92
+
93
+ // Feature: Agent Delegation — TravelAgent delegates to WeatherAgent via @Delegate
94
+ // Try: "Plan a 3-day trip to Tokyo"
95
+ @Post()
96
+ async planTrip(@Body() body: { message: string }) {
97
+ const response = await this.travelAgent.execute(body.message);
98
+ return { response };
99
+ }
100
+
101
+ // Feature: Supervisor — LLM router routes between WeatherAgent and FactsAgent
102
+ // at runtime based on which specialist is most relevant to the request.
103
+ // Try: "What should I pack for a trip to London?" or "Tell me about Paris"
104
+ @Post('supervisor')
105
+ async supervisorRoute(@Body() body: { message: string }) {
106
+ const runtime = this.agentService.getRuntime();
107
+ const supervisor = runtime.createSupervisor({
108
+ name: 'travel-supervisor',
109
+ workers: ['WeatherAgent', 'FactsAgent'],
110
+ systemPrompt: 'You are a travel planning supervisor. Route requests to WeatherAgent for weather/forecast questions and to FactsAgent for city facts, tips, and general travel advice. Combine results when both are relevant.',
111
+ maxRounds: 6,
112
+ });
113
+
114
+ const result = await supervisor.run(body.message);
115
+ return {
116
+ response: result.response,
117
+ rounds: result.rounds.map((r) => ({
118
+ round: r.round,
119
+ worker: r.decision.worker ?? 'supervisor',
120
+ thought: r.decision.thought ?? null,
121
+ })),
122
+ };
123
+ }
124
+ }
@@ -0,0 +1,27 @@
1
+ import { Controller, Post, Body, Service } from '@hazeljs/core';
2
+ import { AIEnhancedService } from '@hazeljs/ai';
3
+
4
+ @Service()
5
+ export class ChatService {
6
+ constructor(private readonly ai: AIEnhancedService) {}
7
+
8
+ async chat(message: string): Promise<string> {
9
+ return this.ai
10
+ .chat(message)
11
+ .system('You are a helpful assistant specializing in HazelJS and TypeScript development.')
12
+ .model('gpt-4')
13
+ .temperature(0.7)
14
+ .text();
15
+ }
16
+ }
17
+
18
+ @Controller('chat')
19
+ export class ChatController {
20
+ constructor(private readonly chatService: ChatService) {}
21
+
22
+ @Post()
23
+ async sendMessage(@Body() body: { message: string }) {
24
+ const response = await this.chatService.chat(body.message);
25
+ return { response };
26
+ }
27
+ }
@@ -0,0 +1,44 @@
1
+ import { HazelModule } from '@hazeljs/core';
2
+ import { InspectorModule } from '@hazeljs/inspector';
3
+ import { AIModule } from '@hazeljs/ai';
4
+ import { AgentModule } from '@hazeljs/agent';
5
+ import { RAGModule } from '@hazeljs/rag';
6
+ import { ConfigModule } from '@hazeljs/config';
7
+ import { CacheModule } from '@hazeljs/cache';
8
+ import { ChatController } from './ai/chat.controller';
9
+ import { RAGController } from './rag/rag.controller';
10
+ import { HealthController } from './health.controller';
11
+
12
+ // Import agent classes BEFORE AgentModule to ensure @Agent decorators run first
13
+ import { AgentController, WeatherAgent } from './agent/agent.controller';
14
+ import { TravelController, TravelAgent, FactsAgent } from './agent/travel.controller';
15
+
16
+ @HazelModule({
17
+ imports: [
18
+ // Inspector - dev tools at /__hazel
19
+ InspectorModule.forRoot({
20
+ inspectorBasePath: '/__hazel',
21
+ developmentOnly: true,
22
+ }) as any,
23
+ // Configuration module
24
+ ConfigModule.forRoot({
25
+ envFilePath: ['.env', '.env.local'],
26
+ isGlobal: true,
27
+ }) as any,
28
+ // Cache Module
29
+ CacheModule.forRoot({
30
+ strategy: 'memory',
31
+ isGlobal: true,
32
+ }) as any,
33
+ // AI Module with OpenAI provider (must come before AgentModule)
34
+ AIModule,
35
+ // RAG Module
36
+ RAGModule,
37
+ // Agent Module - auto-discovers @Agent decorated classes
38
+ // LLM provider is auto-configured from AIEnhancedService (via AIModule)
39
+ AgentModule.forRoot() as any,
40
+ ],
41
+ controllers: [ChatController, RAGController, HealthController, AgentController, TravelController],
42
+ providers: [WeatherAgent, TravelAgent, FactsAgent],
43
+ })
44
+ export class AppModule {}
@@ -0,0 +1,14 @@
1
+ import { Controller, Get } from '@hazeljs/core';
2
+
3
+ @Controller('/')
4
+ export class HealthController {
5
+ @Get('health')
6
+ health() {
7
+ return { status: 'ok', timestamp: new Date().toISOString() };
8
+ }
9
+
10
+ @Get()
11
+ root() {
12
+ return { message: 'HazelJS AI-Native Application', version: '1.0.0' };
13
+ }
14
+ }
@@ -0,0 +1,12 @@
1
+ import { HazelApp } from '@hazeljs/core';
2
+ import { AppModule } from './app.module';
3
+
4
+ async function bootstrap() {
5
+ const app = new HazelApp(AppModule);
6
+ await app.listen(3000);
7
+ console.log('🚀 HazelJS AI-Native app running on http://localhost:3000');
8
+ console.log('📊 Inspector: http://localhost:3000/__hazel');
9
+ console.log('🏥 Health: http://localhost:3000/health');
10
+ }
11
+
12
+ bootstrap();
@@ -0,0 +1,161 @@
1
+ import { Controller, Post, Get, Body, Service } from '@hazeljs/core';
2
+ import { OpenAIEmbeddings } from '@hazeljs/rag';
3
+ import { PrismaClient } from '@prisma/client';
4
+
5
+ @Service()
6
+ export class RAGService {
7
+ private embeddings: OpenAIEmbeddings;
8
+ private prisma: PrismaClient;
9
+
10
+ constructor() {
11
+ // Initialize embeddings for generating vectors
12
+ this.embeddings = new OpenAIEmbeddings({
13
+ apiKey: process.env.OPENAI_API_KEY!,
14
+ model: 'text-embedding-3-small',
15
+ });
16
+
17
+ // Initialize Prisma for PostgreSQL storage
18
+ this.prisma = new PrismaClient();
19
+ }
20
+
21
+ async onModuleInit() {
22
+ await this.prisma.$connect();
23
+ const count = await this.prisma.document.count();
24
+ console.log(`RAG Service initialized - PostgreSQL vector store ready with ${count} documents`);
25
+ }
26
+
27
+ async onModuleDestroy() {
28
+ await this.prisma.$disconnect();
29
+ }
30
+
31
+ async ingestDocument(content: string, metadata?: any) {
32
+ console.log(`Ingesting document, content length: ${content.length}`);
33
+
34
+ // Generate embedding for the document
35
+ const embedding = await this.embeddings.embed(content);
36
+
37
+ // Store in PostgreSQL
38
+ const document = await this.prisma.document.create({
39
+ data: {
40
+ content,
41
+ embedding,
42
+ metadata: {
43
+ source: 'user-input',
44
+ timestamp: new Date().toISOString(),
45
+ ...metadata,
46
+ },
47
+ },
48
+ });
49
+
50
+ const totalDocuments = await this.prisma.document.count();
51
+ console.log(`Document ingested successfully. ID: ${document.id}, Total documents: ${totalDocuments}`);
52
+
53
+ return {
54
+ id: document.id,
55
+ message: 'Document ingested successfully and stored in PostgreSQL',
56
+ totalDocuments
57
+ };
58
+ }
59
+
60
+ async search(query: string, topK = 5) {
61
+ const totalDocuments = await this.prisma.document.count();
62
+ console.log(`RAG search requested for: "${query}". Total documents: ${totalDocuments}`);
63
+
64
+ // Generate embedding for the query
65
+ const queryVector = await this.embeddings.embed(query);
66
+
67
+ // Perform vector similarity search using raw SQL for cosine similarity
68
+ const results = await this.prisma.$queryRaw<Array<{
69
+ id: string;
70
+ content: string;
71
+ metadata: any;
72
+ embedding: number[];
73
+ similarity: number;
74
+ }>>`
75
+ SELECT
76
+ id,
77
+ content,
78
+ metadata,
79
+ embedding,
80
+ (1 - (embedding <=> ${queryVector}::vector)) as similarity
81
+ FROM documents
82
+ ORDER BY embedding <=> ${queryVector}::vector
83
+ LIMIT ${topK}
84
+ `;
85
+
86
+ console.log(`RAG search for "${query}" returned ${results.length} results`);
87
+
88
+ return results.map(result => ({
89
+ id: result.id,
90
+ content: result.content,
91
+ score: result.similarity,
92
+ metadata: result.metadata,
93
+ }));
94
+ }
95
+
96
+ async getDocumentCount() {
97
+ const totalDocuments = await this.prisma.document.count();
98
+ return { totalDocuments };
99
+ }
100
+
101
+ async getAllDocuments() {
102
+ const documents = await this.prisma.document.findMany({
103
+ select: {
104
+ id: true,
105
+ content: true,
106
+ metadata: true,
107
+ createdAt: true,
108
+ },
109
+ orderBy: {
110
+ createdAt: 'desc',
111
+ },
112
+ });
113
+ return documents;
114
+ }
115
+ }
116
+
117
+ @Controller('rag')
118
+ export class RAGController {
119
+ constructor(private readonly ragService: RAGService) {}
120
+
121
+ @Post('ingest')
122
+ async ingestDocument(@Body() body: { content: string; metadata?: any }) {
123
+ const result = await this.ragService.ingestDocument(body.content, body.metadata);
124
+ return result;
125
+ }
126
+
127
+ @Post('search')
128
+ async search(@Body() body: { query: string }) {
129
+ const results = await this.ragService.search(body.query);
130
+ return { results };
131
+ }
132
+
133
+ @Get('documents')
134
+ async getDocuments() {
135
+ const stats = await this.ragService.getDocumentCount();
136
+ return {
137
+ message: 'RAG service is ready - PostgreSQL vector store with persistent storage',
138
+ ...stats,
139
+ endpoints: {
140
+ ingest: 'POST /rag/ingest - Add documents to PostgreSQL',
141
+ search: 'POST /rag/search - Search documents with vector similarity',
142
+ stats: 'GET /rag/stats - Get document count',
143
+ list: 'GET /rag/list - List all documents'
144
+ }
145
+ };
146
+ }
147
+
148
+ @Get('stats')
149
+ async getStats() {
150
+ return this.ragService.getDocumentCount();
151
+ }
152
+
153
+ @Get('list')
154
+ async listDocuments() {
155
+ const documents = await this.ragService.getAllDocuments();
156
+ return {
157
+ documents,
158
+ total: documents.length
159
+ };
160
+ }
161
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "experimentalDecorators": true,
4
+ "emitDecoratorMetadata": true,
5
+ "target": "ES2020",
6
+ "module": "commonjs",
7
+ "lib": ["ES2020"],
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "sourceMap": true
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
21
+ }