neoagent 2.3.0 → 2.3.1-beta.2
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/README.md +1 -1
- package/docs/automation.md +1 -1
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +10 -1
- package/docs/integrations.md +5 -0
- package/package.json +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +17742 -17688
- package/server/routes/integrations.js +11 -8
- package/server/services/ai/engine.js +9 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +120 -10
- package/server/services/integrations/env.js +14 -0
- package/server/services/integrations/home_assistant/provider.js +350 -0
- package/server/services/integrations/registry.js +6 -0
- package/server/services/integrations/spotify/provider.js +487 -0
- package/server/services/integrations/weather/provider.js +559 -0
- package/server/services/messaging/manager.js +29 -7
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/schedule.js +33 -5
- package/server/services/tasks/adapters/weather_event.js +84 -0
- package/server/services/tasks/integration_runtime.js +85 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/agentBridge.js +20 -4
- package/server/services/voice/message.js +3 -0
- package/server/services/voice/runtimeManager.js +136 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const {
|
|
5
|
+
describeEnvStatus,
|
|
6
|
+
resolveHomeAssistantOAuthConfig,
|
|
7
|
+
} = require('../env');
|
|
8
|
+
const {
|
|
9
|
+
appendQuery,
|
|
10
|
+
createOAuthProvider,
|
|
11
|
+
fetchJson,
|
|
12
|
+
} = require('../oauth_provider');
|
|
13
|
+
|
|
14
|
+
const HOME_ASSISTANT_APPS = [
|
|
15
|
+
{
|
|
16
|
+
id: 'home_assistant',
|
|
17
|
+
label: 'Home Assistant',
|
|
18
|
+
description: 'Connect Home Assistant for entity state queries, service calls, and API access.',
|
|
19
|
+
scopes: ['homeassistant'],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const homeAssistantToolDefinitions = [
|
|
24
|
+
{
|
|
25
|
+
appId: 'home_assistant',
|
|
26
|
+
name: 'home_assistant_get_config',
|
|
27
|
+
access: 'read',
|
|
28
|
+
description: 'Get Home Assistant configuration details for the connected instance.',
|
|
29
|
+
parameters: { type: 'object', properties: {} },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
appId: 'home_assistant',
|
|
33
|
+
name: 'home_assistant_list_states',
|
|
34
|
+
access: 'read',
|
|
35
|
+
description: 'List entity states from Home Assistant. Optional filters are applied client-side.',
|
|
36
|
+
parameters: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
domain: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Optional entity domain filter, for example light, climate, or switch.',
|
|
42
|
+
},
|
|
43
|
+
limit: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description: 'Optional max entities to return, default 100.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
appId: 'home_assistant',
|
|
52
|
+
name: 'home_assistant_get_state',
|
|
53
|
+
access: 'read',
|
|
54
|
+
description: 'Get state for a single Home Assistant entity by entity_id.',
|
|
55
|
+
parameters: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
entity_id: { type: 'string', description: 'Entity ID, for example light.living_room.' },
|
|
59
|
+
},
|
|
60
|
+
required: ['entity_id'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
appId: 'home_assistant',
|
|
65
|
+
name: 'home_assistant_call_service',
|
|
66
|
+
access: 'write',
|
|
67
|
+
description: 'Call a Home Assistant service such as light.turn_on or climate.set_temperature.',
|
|
68
|
+
parameters: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
domain: { type: 'string', description: 'Service domain, for example light, climate, or switch.' },
|
|
72
|
+
service: { type: 'string', description: 'Service name, for example turn_on, turn_off, or set_temperature.' },
|
|
73
|
+
service_data: { type: 'object', description: 'Optional Home Assistant service data payload.' },
|
|
74
|
+
},
|
|
75
|
+
required: ['domain', 'service'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
appId: 'home_assistant',
|
|
80
|
+
name: 'home_assistant_api_request',
|
|
81
|
+
access: 'dynamic_http_method',
|
|
82
|
+
description: 'Make an authenticated Home Assistant REST API request for advanced operations.',
|
|
83
|
+
parameters: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
|
|
87
|
+
path: { type: 'string', description: 'API path under the configured instance, for example /api/history/period.' },
|
|
88
|
+
query: { type: 'object', description: 'Optional query parameters.' },
|
|
89
|
+
body: { type: 'object', description: 'Optional JSON body.' },
|
|
90
|
+
},
|
|
91
|
+
required: ['method', 'path'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function requireText(value, label) {
|
|
97
|
+
const text = String(value || '').trim();
|
|
98
|
+
if (!text) throw new Error(`${label} is required.`);
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function homeAssistantUrl(baseUrl, path, query) {
|
|
103
|
+
const normalizedBase = String(baseUrl || '').trim().replace(/\/$/, '');
|
|
104
|
+
if (!normalizedBase) {
|
|
105
|
+
throw new Error('HOME_ASSISTANT_BASE_URL is required.');
|
|
106
|
+
}
|
|
107
|
+
const url = new URL(
|
|
108
|
+
String(path || '').startsWith('http')
|
|
109
|
+
? String(path)
|
|
110
|
+
: `${normalizedBase}${String(path || '').startsWith('/') ? '' : '/'}${path}`,
|
|
111
|
+
);
|
|
112
|
+
if (new URL(normalizedBase).origin !== url.origin) {
|
|
113
|
+
throw new Error('Home Assistant request URL must stay on HOME_ASSISTANT_BASE_URL origin.');
|
|
114
|
+
}
|
|
115
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
116
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
117
|
+
}
|
|
118
|
+
return url.toString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function homeAssistantRequest(credentials, options = {}) {
|
|
122
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
123
|
+
const accessToken = String(credentials?.access_token || '').trim();
|
|
124
|
+
if (!accessToken) {
|
|
125
|
+
throw new Error('Home Assistant access token is missing. Reconnect this integration account.');
|
|
126
|
+
}
|
|
127
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
128
|
+
return fetchJson(
|
|
129
|
+
homeAssistantUrl(config.baseUrl, options.path, options.query),
|
|
130
|
+
{
|
|
131
|
+
method,
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${accessToken}`,
|
|
134
|
+
},
|
|
135
|
+
...(options.body === undefined ? {} : { json: options.body }),
|
|
136
|
+
},
|
|
137
|
+
{ serviceName: 'Home Assistant' },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
142
|
+
switch (toolName) {
|
|
143
|
+
case 'home_assistant_get_config':
|
|
144
|
+
return {
|
|
145
|
+
result: await homeAssistantRequest(credentials, { path: '/api/config' }),
|
|
146
|
+
};
|
|
147
|
+
case 'home_assistant_list_states': {
|
|
148
|
+
const states = await homeAssistantRequest(credentials, { path: '/api/states' });
|
|
149
|
+
const domain = String(args.domain || '').trim().toLowerCase();
|
|
150
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
|
|
151
|
+
const filtered = Array.isArray(states)
|
|
152
|
+
? states.filter((item) => {
|
|
153
|
+
if (!domain) return true;
|
|
154
|
+
const entityId = String(item?.entity_id || '').toLowerCase();
|
|
155
|
+
return entityId.startsWith(`${domain}.`);
|
|
156
|
+
})
|
|
157
|
+
: [];
|
|
158
|
+
return {
|
|
159
|
+
result: filtered.slice(0, limit),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
case 'home_assistant_get_state':
|
|
163
|
+
return {
|
|
164
|
+
result: await homeAssistantRequest(credentials, {
|
|
165
|
+
path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
case 'home_assistant_call_service':
|
|
169
|
+
return {
|
|
170
|
+
result: await homeAssistantRequest(credentials, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
path: `/api/services/${encodeURIComponent(requireText(args.domain, 'domain'))}/${encodeURIComponent(requireText(args.service, 'service'))}`,
|
|
173
|
+
body: args.service_data || {},
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
case 'home_assistant_api_request':
|
|
177
|
+
return {
|
|
178
|
+
result: await homeAssistantRequest(credentials, {
|
|
179
|
+
method: args.method,
|
|
180
|
+
path: requireText(args.path, 'path'),
|
|
181
|
+
query: args.query,
|
|
182
|
+
body: args.body,
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
default:
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveHomeAssistantEnvStatus() {
|
|
191
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
192
|
+
const missing = config.missing.slice();
|
|
193
|
+
if (!config.baseUrl) {
|
|
194
|
+
missing.push('HOME_ASSISTANT_BASE_URL');
|
|
195
|
+
}
|
|
196
|
+
return describeEnvStatus(
|
|
197
|
+
{
|
|
198
|
+
configured: missing.length === 0,
|
|
199
|
+
missing,
|
|
200
|
+
},
|
|
201
|
+
{ label: 'Home Assistant' },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeCurrentUser(currentUser) {
|
|
206
|
+
const payload = currentUser && typeof currentUser === 'object' ? currentUser : {};
|
|
207
|
+
return {
|
|
208
|
+
id: payload.id || null,
|
|
209
|
+
name: payload.name || null,
|
|
210
|
+
username: payload.username || null,
|
|
211
|
+
email: payload.email || null,
|
|
212
|
+
isOwner: Boolean(payload.is_owner),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stableAccountEmailLikeIdentifier(user, config) {
|
|
217
|
+
const normalized = normalizeCurrentUser(user);
|
|
218
|
+
const preferred = [normalized.email, normalized.username, normalized.id, normalized.name]
|
|
219
|
+
.map((value) => String(value || '').trim())
|
|
220
|
+
.find(Boolean);
|
|
221
|
+
if (preferred) {
|
|
222
|
+
return preferred;
|
|
223
|
+
}
|
|
224
|
+
const host = new URL(config.baseUrl).host;
|
|
225
|
+
return `homeassistant@${host}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function fetchCurrentUser(token) {
|
|
229
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
230
|
+
return fetchJson(
|
|
231
|
+
homeAssistantUrl(config.baseUrl, '/api/auth/current_user'),
|
|
232
|
+
{
|
|
233
|
+
method: 'GET',
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${token}`,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{ serviceName: 'Home Assistant' },
|
|
239
|
+
).catch(() => null);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function createHomeAssistantProvider() {
|
|
243
|
+
return createOAuthProvider({
|
|
244
|
+
key: 'home_assistant',
|
|
245
|
+
label: 'Home Assistant',
|
|
246
|
+
description:
|
|
247
|
+
'Official Home Assistant account connections for entity state reads, service control, and automation support.',
|
|
248
|
+
icon: 'home_assistant',
|
|
249
|
+
apps: HOME_ASSISTANT_APPS,
|
|
250
|
+
toolDefinitions: homeAssistantToolDefinitions,
|
|
251
|
+
connectPrompt:
|
|
252
|
+
'Connect your Home Assistant account to let the agent read entity states and control services with structured tools.',
|
|
253
|
+
getEnvStatus() {
|
|
254
|
+
return resolveHomeAssistantEnvStatus();
|
|
255
|
+
},
|
|
256
|
+
async beginOAuth({ state, codeVerifier, app }) {
|
|
257
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
258
|
+
const codeChallenge = String(codeChallengeForVerifier(codeVerifier));
|
|
259
|
+
return {
|
|
260
|
+
url: appendQuery(homeAssistantUrl(config.baseUrl, '/auth/authorize'), {
|
|
261
|
+
response_type: 'code',
|
|
262
|
+
client_id: config.clientId,
|
|
263
|
+
redirect_uri: config.redirectUri,
|
|
264
|
+
state,
|
|
265
|
+
scope: app.scopes.join(' '),
|
|
266
|
+
code_challenge: codeChallenge,
|
|
267
|
+
code_challenge_method: 'S256',
|
|
268
|
+
}),
|
|
269
|
+
appId: app.id,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
async finishOAuth({ code, codeVerifier, app }) {
|
|
273
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
274
|
+
const token = await fetchJson(
|
|
275
|
+
homeAssistantUrl(config.baseUrl, '/auth/token'),
|
|
276
|
+
{
|
|
277
|
+
method: 'POST',
|
|
278
|
+
form: {
|
|
279
|
+
grant_type: 'authorization_code',
|
|
280
|
+
client_id: config.clientId,
|
|
281
|
+
client_secret: config.clientSecret,
|
|
282
|
+
code,
|
|
283
|
+
code_verifier: codeVerifier,
|
|
284
|
+
redirect_uri: config.redirectUri,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{ serviceName: 'Home Assistant' },
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const accessToken = String(token?.access_token || '').trim();
|
|
291
|
+
if (!accessToken) {
|
|
292
|
+
throw new Error('Home Assistant OAuth did not return an access token.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const refreshToken = String(token?.refresh_token || '').trim();
|
|
296
|
+
if (!refreshToken) {
|
|
297
|
+
throw new Error('Home Assistant OAuth did not return a refresh token.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const currentUser = await fetchCurrentUser(accessToken);
|
|
301
|
+
const normalizedUser = normalizeCurrentUser(currentUser);
|
|
302
|
+
const accountEmail = stableAccountEmailLikeIdentifier(normalizedUser, config);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
appId: app.id,
|
|
306
|
+
accountEmail,
|
|
307
|
+
credentials: {
|
|
308
|
+
access_token: accessToken,
|
|
309
|
+
refresh_token: refreshToken,
|
|
310
|
+
token_type: token?.token_type || 'Bearer',
|
|
311
|
+
expires_in: token?.expires_in || null,
|
|
312
|
+
scope: token?.scope || app.scopes.join(' '),
|
|
313
|
+
},
|
|
314
|
+
scopes: Array.isArray(token?.scope)
|
|
315
|
+
? token.scope
|
|
316
|
+
: String(token?.scope || app.scopes.join(' '))
|
|
317
|
+
.split(/\s+/)
|
|
318
|
+
.map((scope) => scope.trim())
|
|
319
|
+
.filter(Boolean),
|
|
320
|
+
metadata: {
|
|
321
|
+
homeAssistantUrl: config.baseUrl,
|
|
322
|
+
userId: normalizedUser.id,
|
|
323
|
+
name: normalizedUser.name,
|
|
324
|
+
username: normalizedUser.username,
|
|
325
|
+
email: normalizedUser.email,
|
|
326
|
+
isOwner: normalizedUser.isOwner,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
executeTool: executeHomeAssistantTool,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function base64UrlSha256(value) {
|
|
335
|
+
return crypto
|
|
336
|
+
.createHash('sha256')
|
|
337
|
+
.update(String(value || ''))
|
|
338
|
+
.digest('base64')
|
|
339
|
+
.replace(/\+/g, '-')
|
|
340
|
+
.replace(/\//g, '_')
|
|
341
|
+
.replace(/=+$/g, '');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function codeChallengeForVerifier(codeVerifier) {
|
|
345
|
+
return base64UrlSha256(String(codeVerifier || ''));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
createHomeAssistantProvider,
|
|
350
|
+
};
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { createFigmaProvider } = require('./figma/provider');
|
|
4
4
|
const { createGoogleWorkspaceProvider } = require('./google/provider');
|
|
5
|
+
const { createHomeAssistantProvider } = require('./home_assistant/provider');
|
|
5
6
|
const { createMicrosoftProvider } = require('./microsoft/provider');
|
|
6
7
|
const { createNotionProvider } = require('./notion/provider');
|
|
8
|
+
const { createSpotifyProvider } = require('./spotify/provider');
|
|
7
9
|
const { createSlackProvider } = require('./slack/provider');
|
|
10
|
+
const { createWeatherProvider } = require('./weather/provider');
|
|
8
11
|
const { createWhatsAppPersonalProvider } = require('./whatsapp');
|
|
9
12
|
|
|
10
13
|
function createIntegrationRegistry(options = {}) {
|
|
@@ -14,6 +17,9 @@ function createIntegrationRegistry(options = {}) {
|
|
|
14
17
|
createMicrosoftProvider(),
|
|
15
18
|
createSlackProvider(),
|
|
16
19
|
createFigmaProvider(),
|
|
20
|
+
createHomeAssistantProvider(),
|
|
21
|
+
createWeatherProvider(),
|
|
22
|
+
createSpotifyProvider(),
|
|
17
23
|
createWhatsAppPersonalProvider(options),
|
|
18
24
|
];
|
|
19
25
|
const byKey = new Map(providers.map((provider) => [provider.key, provider]));
|