@coffeerunhobby/mcp-ha-connect 0.5.0

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 (294) hide show
  1. package/.dockerignore +8 -0
  2. package/.env.example +149 -0
  3. package/.gitattributes +30 -0
  4. package/Dockerfile +33 -0
  5. package/LICENSE +21 -0
  6. package/README.md +394 -0
  7. package/dist/config.d.ts +33 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +183 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/haClient/automations.d.ts +67 -0
  12. package/dist/haClient/automations.d.ts.map +1 -0
  13. package/dist/haClient/automations.js +181 -0
  14. package/dist/haClient/automations.js.map +1 -0
  15. package/dist/haClient/config.d.ts +28 -0
  16. package/dist/haClient/config.d.ts.map +1 -0
  17. package/dist/haClient/config.js +38 -0
  18. package/dist/haClient/config.js.map +1 -0
  19. package/dist/haClient/devices.d.ts +219 -0
  20. package/dist/haClient/devices.d.ts.map +1 -0
  21. package/dist/haClient/devices.js +506 -0
  22. package/dist/haClient/devices.js.map +1 -0
  23. package/dist/haClient/entities.d.ts +35 -0
  24. package/dist/haClient/entities.d.ts.map +1 -0
  25. package/dist/haClient/entities.js +97 -0
  26. package/dist/haClient/entities.js.map +1 -0
  27. package/dist/haClient/events.d.ts +138 -0
  28. package/dist/haClient/events.d.ts.map +1 -0
  29. package/dist/haClient/events.js +344 -0
  30. package/dist/haClient/events.js.map +1 -0
  31. package/dist/haClient/history.d.ts +26 -0
  32. package/dist/haClient/history.d.ts.map +1 -0
  33. package/dist/haClient/history.js +54 -0
  34. package/dist/haClient/history.js.map +1 -0
  35. package/dist/haClient/index.d.ts +153 -0
  36. package/dist/haClient/index.d.ts.map +1 -0
  37. package/dist/haClient/index.js +223 -0
  38. package/dist/haClient/index.js.map +1 -0
  39. package/dist/haClient/request.d.ts +34 -0
  40. package/dist/haClient/request.d.ts.map +1 -0
  41. package/dist/haClient/request.js +93 -0
  42. package/dist/haClient/request.js.map +1 -0
  43. package/dist/haClient/services.d.ts +22 -0
  44. package/dist/haClient/services.d.ts.map +1 -0
  45. package/dist/haClient/services.js +39 -0
  46. package/dist/haClient/services.js.map +1 -0
  47. package/dist/haClient/states.d.ts +26 -0
  48. package/dist/haClient/states.d.ts.map +1 -0
  49. package/dist/haClient/states.js +53 -0
  50. package/dist/haClient/states.js.map +1 -0
  51. package/dist/haClient/updates.d.ts +22 -0
  52. package/dist/haClient/updates.d.ts.map +1 -0
  53. package/dist/haClient/updates.js +48 -0
  54. package/dist/haClient/updates.js.map +1 -0
  55. package/dist/haClient.d.ts +97 -0
  56. package/dist/haClient.d.ts.map +1 -0
  57. package/dist/haClient.js +344 -0
  58. package/dist/haClient.js.map +1 -0
  59. package/dist/index.d.ts +8 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +92 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/localAI/index.d.ts +37 -0
  64. package/dist/localAI/index.d.ts.map +1 -0
  65. package/dist/localAI/index.js +73 -0
  66. package/dist/localAI/index.js.map +1 -0
  67. package/dist/localAI/prompts.d.ts +20 -0
  68. package/dist/localAI/prompts.d.ts.map +1 -0
  69. package/dist/localAI/prompts.js +48 -0
  70. package/dist/localAI/prompts.js.map +1 -0
  71. package/dist/localAI/providers/base.d.ts +51 -0
  72. package/dist/localAI/providers/base.d.ts.map +1 -0
  73. package/dist/localAI/providers/base.js +80 -0
  74. package/dist/localAI/providers/base.js.map +1 -0
  75. package/dist/localAI/providers/ollama.d.ts +22 -0
  76. package/dist/localAI/providers/ollama.d.ts.map +1 -0
  77. package/dist/localAI/providers/ollama.js +63 -0
  78. package/dist/localAI/providers/ollama.js.map +1 -0
  79. package/dist/localAI/providers/openai.d.ts +23 -0
  80. package/dist/localAI/providers/openai.d.ts.map +1 -0
  81. package/dist/localAI/providers/openai.js +63 -0
  82. package/dist/localAI/providers/openai.js.map +1 -0
  83. package/dist/localAI/types.d.ts +61 -0
  84. package/dist/localAI/types.d.ts.map +1 -0
  85. package/dist/localAI/types.js +6 -0
  86. package/dist/localAI/types.js.map +1 -0
  87. package/dist/ollamaClient.d.ts +37 -0
  88. package/dist/ollamaClient.d.ts.map +1 -0
  89. package/dist/ollamaClient.js +110 -0
  90. package/dist/ollamaClient.js.map +1 -0
  91. package/dist/resources/index.d.ts +11 -0
  92. package/dist/resources/index.d.ts.map +1 -0
  93. package/dist/resources/index.js +238 -0
  94. package/dist/resources/index.js.map +1 -0
  95. package/dist/server/common.d.ts +9 -0
  96. package/dist/server/common.d.ts.map +1 -0
  97. package/dist/server/common.js +27 -0
  98. package/dist/server/common.js.map +1 -0
  99. package/dist/server/eventSubscription.d.ts +53 -0
  100. package/dist/server/eventSubscription.d.ts.map +1 -0
  101. package/dist/server/eventSubscription.js +257 -0
  102. package/dist/server/eventSubscription.js.map +1 -0
  103. package/dist/server/http.d.ts +12 -0
  104. package/dist/server/http.d.ts.map +1 -0
  105. package/dist/server/http.js +491 -0
  106. package/dist/server/http.js.map +1 -0
  107. package/dist/server/rateLimiter.d.ts +70 -0
  108. package/dist/server/rateLimiter.d.ts.map +1 -0
  109. package/dist/server/rateLimiter.js +156 -0
  110. package/dist/server/rateLimiter.js.map +1 -0
  111. package/dist/server/sse.d.ts +32 -0
  112. package/dist/server/sse.d.ts.map +1 -0
  113. package/dist/server/sse.js +76 -0
  114. package/dist/server/sse.js.map +1 -0
  115. package/dist/server/stdio.d.ts +8 -0
  116. package/dist/server/stdio.d.ts.map +1 -0
  117. package/dist/server/stdio.js +16 -0
  118. package/dist/server/stdio.js.map +1 -0
  119. package/dist/server/stream.d.ts +26 -0
  120. package/dist/server/stream.d.ts.map +1 -0
  121. package/dist/server/stream.js +81 -0
  122. package/dist/server/stream.js.map +1 -0
  123. package/dist/server.d.ts +8 -0
  124. package/dist/server.d.ts.map +1 -0
  125. package/dist/server.js +30 -0
  126. package/dist/server.js.map +1 -0
  127. package/dist/tools/callService.d.ts +8 -0
  128. package/dist/tools/callService.d.ts.map +1 -0
  129. package/dist/tools/callService.js +100 -0
  130. package/dist/tools/callService.js.map +1 -0
  131. package/dist/tools/getClient.d.ts +8 -0
  132. package/dist/tools/getClient.d.ts.map +1 -0
  133. package/dist/tools/getClient.js +94 -0
  134. package/dist/tools/getClient.js.map +1 -0
  135. package/dist/tools/getDevice.d.ts +8 -0
  136. package/dist/tools/getDevice.d.ts.map +1 -0
  137. package/dist/tools/getDevice.js +94 -0
  138. package/dist/tools/getDevice.js.map +1 -0
  139. package/dist/tools/getEntitiesByDomain.d.ts +8 -0
  140. package/dist/tools/getEntitiesByDomain.d.ts.map +1 -0
  141. package/dist/tools/getEntitiesByDomain.js +80 -0
  142. package/dist/tools/getEntitiesByDomain.js.map +1 -0
  143. package/dist/tools/getState.d.ts +8 -0
  144. package/dist/tools/getState.d.ts.map +1 -0
  145. package/dist/tools/getState.js +90 -0
  146. package/dist/tools/getState.js.map +1 -0
  147. package/dist/tools/getStates.d.ts +8 -0
  148. package/dist/tools/getStates.d.ts.map +1 -0
  149. package/dist/tools/getStates.js +70 -0
  150. package/dist/tools/getStates.js.map +1 -0
  151. package/dist/tools/index.d.ts +9 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +9 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/listClients.d.ts +8 -0
  156. package/dist/tools/listClients.d.ts.map +1 -0
  157. package/dist/tools/listClients.js +73 -0
  158. package/dist/tools/listClients.js.map +1 -0
  159. package/dist/tools/listDevices.d.ts +8 -0
  160. package/dist/tools/listDevices.d.ts.map +1 -0
  161. package/dist/tools/listDevices.js +73 -0
  162. package/dist/tools/listDevices.js.map +1 -0
  163. package/dist/tools/listSites.d.ts +8 -0
  164. package/dist/tools/listSites.d.ts.map +1 -0
  165. package/dist/tools/listSites.js +67 -0
  166. package/dist/tools/listSites.js.map +1 -0
  167. package/dist/tools/registry.d.ts +9 -0
  168. package/dist/tools/registry.d.ts.map +1 -0
  169. package/dist/tools/registry.js +1382 -0
  170. package/dist/tools/registry.js.map +1 -0
  171. package/dist/tools/searchDevices.d.ts +8 -0
  172. package/dist/tools/searchDevices.d.ts.map +1 -0
  173. package/dist/tools/searchDevices.js +84 -0
  174. package/dist/tools/searchDevices.js.map +1 -0
  175. package/dist/tools/searchEntities.d.ts +8 -0
  176. package/dist/tools/searchEntities.d.ts.map +1 -0
  177. package/dist/tools/searchEntities.js +80 -0
  178. package/dist/tools/searchEntities.js.map +1 -0
  179. package/dist/types/entity.d.ts +98 -0
  180. package/dist/types/entity.d.ts.map +1 -0
  181. package/dist/types/entity.js +2 -0
  182. package/dist/types/entity.js.map +1 -0
  183. package/dist/types/errors.d.ts +16 -0
  184. package/dist/types/errors.d.ts.map +1 -0
  185. package/dist/types/errors.js +32 -0
  186. package/dist/types/errors.js.map +1 -0
  187. package/dist/types/haConfig.d.ts +8 -0
  188. package/dist/types/haConfig.d.ts.map +1 -0
  189. package/dist/types/haConfig.js +2 -0
  190. package/dist/types/haConfig.js.map +1 -0
  191. package/dist/types/index.d.ts +6 -0
  192. package/dist/types/index.d.ts.map +1 -0
  193. package/dist/types/index.js +2 -0
  194. package/dist/types/index.js.map +1 -0
  195. package/dist/types/request.d.ts +7 -0
  196. package/dist/types/request.d.ts.map +1 -0
  197. package/dist/types/request.js +2 -0
  198. package/dist/types/request.js.map +1 -0
  199. package/dist/types/serviceCall.d.ts +18 -0
  200. package/dist/types/serviceCall.d.ts.map +1 -0
  201. package/dist/types/serviceCall.js +2 -0
  202. package/dist/types/serviceCall.js.map +1 -0
  203. package/dist/types.d.ts +2 -0
  204. package/dist/types.d.ts.map +1 -0
  205. package/dist/types.js +3 -0
  206. package/dist/types.js.map +1 -0
  207. package/dist/utils/config-validations.d.ts +12 -0
  208. package/dist/utils/config-validations.d.ts.map +1 -0
  209. package/dist/utils/config-validations.js +43 -0
  210. package/dist/utils/config-validations.js.map +1 -0
  211. package/dist/utils/logger.d.ts +24 -0
  212. package/dist/utils/logger.d.ts.map +1 -0
  213. package/dist/utils/logger.js +94 -0
  214. package/dist/utils/logger.js.map +1 -0
  215. package/dist/wrapper.d.ts +8 -0
  216. package/dist/wrapper.d.ts.map +1 -0
  217. package/dist/wrapper.js +301 -0
  218. package/dist/wrapper.js.map +1 -0
  219. package/docker-compose.yml +17 -0
  220. package/docs/CHANGELOG.md +86 -0
  221. package/docs/LOCAL_CLIENT.md +268 -0
  222. package/docs/SSE_API.md +341 -0
  223. package/eslint.config.js +33 -0
  224. package/n8n/ai-agent-safety-monitor.json +355 -0
  225. package/openapi.json +166 -0
  226. package/package.json +51 -0
  227. package/src/config.ts +260 -0
  228. package/src/haClient/automations.ts +217 -0
  229. package/src/haClient/config.ts +41 -0
  230. package/src/haClient/devices.ts +653 -0
  231. package/src/haClient/entities.ts +128 -0
  232. package/src/haClient/events.ts +456 -0
  233. package/src/haClient/history.ts +68 -0
  234. package/src/haClient/index.ts +287 -0
  235. package/src/haClient/request.ts +127 -0
  236. package/src/haClient/services.ts +45 -0
  237. package/src/haClient/states.ts +60 -0
  238. package/src/haClient/updates.ts +58 -0
  239. package/src/index.ts +99 -0
  240. package/src/localAI/index.ts +88 -0
  241. package/src/localAI/prompts.ts +50 -0
  242. package/src/localAI/providers/base.ts +120 -0
  243. package/src/localAI/providers/ollama.ts +78 -0
  244. package/src/localAI/providers/openai.ts +86 -0
  245. package/src/localAI/types.ts +67 -0
  246. package/src/resources/index.ts +266 -0
  247. package/src/server/common.ts +38 -0
  248. package/src/server/eventSubscription.ts +318 -0
  249. package/src/server/http.ts +546 -0
  250. package/src/server/rateLimiter.ts +195 -0
  251. package/src/server/sse.ts +120 -0
  252. package/src/server/stdio.ts +22 -0
  253. package/src/server/stream.ts +113 -0
  254. package/src/tools/callService.ts +114 -0
  255. package/src/tools/getEntitiesByDomain.ts +92 -0
  256. package/src/tools/getState.ts +103 -0
  257. package/src/tools/getStates.ts +80 -0
  258. package/src/tools/index.ts +9 -0
  259. package/src/tools/registry.ts +1521 -0
  260. package/src/tools/searchEntities.ts +92 -0
  261. package/src/types/entity.ts +110 -0
  262. package/src/types/errors.ts +33 -0
  263. package/src/types/haConfig.ts +7 -0
  264. package/src/types/index.ts +5 -0
  265. package/src/types/request.ts +6 -0
  266. package/src/types/serviceCall.ts +18 -0
  267. package/src/types.ts +2 -0
  268. package/src/utils/config-validations.ts +46 -0
  269. package/src/utils/logger.ts +114 -0
  270. package/src/wrapper.ts +340 -0
  271. package/tests/config.test.ts +332 -0
  272. package/tests/haClient/automations.test.ts +133 -0
  273. package/tests/haClient/config.test.ts +96 -0
  274. package/tests/haClient/entities.test.ts +188 -0
  275. package/tests/haClient/history.test.ts +170 -0
  276. package/tests/haClient/index.test.ts +221 -0
  277. package/tests/haClient/request.test.ts +217 -0
  278. package/tests/haClient/services.test.ts +120 -0
  279. package/tests/haClient/states.test.ts +119 -0
  280. package/tests/haClient/updates.test.ts +193 -0
  281. package/tests/index.test.ts +164 -0
  282. package/tests/localAI/index.test.ts +168 -0
  283. package/tests/localAI/ollama.test.ts +187 -0
  284. package/tests/localAI/openai.test.ts +257 -0
  285. package/tests/server/common.test.ts +64 -0
  286. package/tests/server/http.test.ts +326 -0
  287. package/tests/server/sse.test.ts +240 -0
  288. package/tests/server/stdio.test.ts +102 -0
  289. package/tests/server/stream.test.ts +287 -0
  290. package/tests/tools/registry.test.ts +355 -0
  291. package/tests/utils/config-validations.test.ts +289 -0
  292. package/tests/utils/logger.test.ts +327 -0
  293. package/tsconfig.json +25 -0
  294. package/vitest.config.ts +26 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Local AI Client
3
+ * Factory and facade for AI providers
4
+ */
5
+
6
+ import { logger } from '../utils/logger.js';
7
+ import type { EnvironmentConfig } from '../config.js';
8
+ import type { AIProviderConfig, AIProviderInterface, AnalysisResult, SensorData } from './types.js';
9
+ import { OllamaProvider } from './providers/ollama.js';
10
+ import { OpenAICompatibleProvider } from './providers/openai.js';
11
+
12
+ // Re-export types for convenience
13
+ export type { SensorData, AnalysisResult, AIProviderType } from './types.js';
14
+
15
+ /**
16
+ * Create the appropriate AI provider based on configuration
17
+ */
18
+ function createProvider(providerConfig: AIProviderConfig): AIProviderInterface {
19
+ switch (providerConfig.provider) {
20
+ case 'openai':
21
+ return new OpenAICompatibleProvider(providerConfig);
22
+ case 'ollama':
23
+ default:
24
+ return new OllamaProvider(providerConfig);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Local AI Client
30
+ * Facade for AI analysis using configurable providers
31
+ */
32
+ export class LocalAIClient {
33
+ private provider: AIProviderInterface;
34
+ private providerConfig: AIProviderConfig;
35
+
36
+ constructor(config: EnvironmentConfig) {
37
+ this.providerConfig = {
38
+ provider: config.aiProvider,
39
+ baseUrl: config.aiUrl,
40
+ model: config.aiModel,
41
+ timeout: config.aiTimeout,
42
+ apiKey: config.aiApiKey,
43
+ };
44
+
45
+ this.provider = createProvider(this.providerConfig);
46
+
47
+ logger.info('LocalAIClient initialized', {
48
+ provider: this.providerConfig.provider,
49
+ model: this.providerConfig.model,
50
+ url: this.providerConfig.baseUrl,
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Analyze sensor data using AI
56
+ */
57
+ async analyzeSensors(sensors: SensorData): Promise<AnalysisResult> {
58
+ return this.provider.analyzeSensors(sensors);
59
+ }
60
+
61
+ /**
62
+ * Check if the AI provider is available
63
+ */
64
+ async checkHealth(): Promise<boolean> {
65
+ return this.provider.checkHealth();
66
+ }
67
+
68
+ /**
69
+ * Get the provider name
70
+ */
71
+ getProviderName(): string {
72
+ return this.provider.getProviderName();
73
+ }
74
+
75
+ /**
76
+ * Get the configured model name
77
+ */
78
+ getModelName(): string {
79
+ return this.providerConfig.model;
80
+ }
81
+
82
+ /**
83
+ * Get the provider configuration
84
+ */
85
+ getConfig(): Readonly<AIProviderConfig> {
86
+ return { ...this.providerConfig };
87
+ }
88
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * AI System Prompts
3
+ * Shared prompts for sensor analysis across all providers
4
+ */
5
+
6
+ /**
7
+ * System prompt for sensor analysis
8
+ */
9
+ export const SENSOR_ANALYSIS_SYSTEM_PROMPT = `You are a home automation AI analyzing Home Assistant sensors.
10
+
11
+ SEVERITY RULES:
12
+ - critical: Gas/smoke/CO detected, fire, temp>40°C indoors, power spike>500%, door open>2h with alarm
13
+ - high: Battery<10%, power spike>300%, door open>30min with alarm, temp>35°C
14
+ - medium: Battery<20%, connectivity issues, minor anomalies
15
+ - low: Informational warnings
16
+
17
+ OUTPUT FORMAT (JSON only, no markdown):
18
+ {
19
+ "severity": "critical|high|medium|low",
20
+ "issue": "brief description",
21
+ "reasoning": "detailed explanation",
22
+ "recommended_actions": ["action1", "action2"],
23
+ "ssh_diagnostics": ["command1", "command2"],
24
+ "confidence": 0.85
25
+ }
26
+
27
+ Respond with JSON only. No extra text.`;
28
+
29
+ /**
30
+ * Build the full prompt for sensor analysis
31
+ */
32
+ export function buildSensorAnalysisPrompt(sensors: Record<string, unknown>): string {
33
+ return `${SENSOR_ANALYSIS_SYSTEM_PROMPT}\n\nAnalyze these sensors: ${JSON.stringify(sensors)}\n\nProvide JSON only:`;
34
+ }
35
+
36
+ /**
37
+ * Build messages array for OpenAI-compatible APIs
38
+ */
39
+ export function buildChatMessages(sensors: Record<string, unknown>): Array<{ role: string; content: string }> {
40
+ return [
41
+ {
42
+ role: 'system',
43
+ content: SENSOR_ANALYSIS_SYSTEM_PROMPT,
44
+ },
45
+ {
46
+ role: 'user',
47
+ content: `Analyze these sensors: ${JSON.stringify(sensors)}\n\nProvide JSON only:`,
48
+ },
49
+ ];
50
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Base AI Provider
3
+ * Abstract class with shared logic for all AI providers
4
+ */
5
+
6
+ import { logger } from '../../utils/logger.js';
7
+ import type { AIProviderConfig, AIProviderInterface, AnalysisResult, SensorData } from '../types.js';
8
+
9
+ /**
10
+ * Raw analysis result from AI (before adding metadata)
11
+ */
12
+ export interface RawAnalysisResult {
13
+ severity: 'critical' | 'high' | 'medium' | 'low';
14
+ issue: string;
15
+ reasoning: string;
16
+ recommended_actions: string[];
17
+ ssh_diagnostics: string[];
18
+ confidence: number;
19
+ }
20
+
21
+ /**
22
+ * Abstract base class for AI providers
23
+ */
24
+ export abstract class BaseAIProvider implements AIProviderInterface {
25
+ protected baseUrl: string;
26
+ protected model: string;
27
+ protected timeout: number;
28
+ protected apiKey?: string;
29
+
30
+ constructor(config: AIProviderConfig) {
31
+ this.baseUrl = config.baseUrl;
32
+ this.model = config.model;
33
+ this.timeout = config.timeout;
34
+ this.apiKey = config.apiKey;
35
+ }
36
+
37
+ /**
38
+ * Get the provider name - must be implemented by subclasses
39
+ */
40
+ abstract getProviderName(): string;
41
+
42
+ /**
43
+ * Make the actual API request - must be implemented by subclasses
44
+ */
45
+ protected abstract makeRequest(sensors: SensorData): Promise<string>;
46
+
47
+ /**
48
+ * Check health - must be implemented by subclasses
49
+ */
50
+ abstract checkHealth(): Promise<boolean>;
51
+
52
+ /**
53
+ * Analyze sensors using AI
54
+ */
55
+ async analyzeSensors(sensors: SensorData): Promise<AnalysisResult> {
56
+ const sensorCount = Object.keys(sensors).length;
57
+ const providerName = this.getProviderName();
58
+
59
+ logger.info('Starting AI analysis', { sensorCount, model: this.model, provider: providerName });
60
+
61
+ const startTime = Date.now();
62
+
63
+ try {
64
+ const aiText = await this.makeRequest(sensors);
65
+
66
+ if (!aiText) {
67
+ throw new Error(`Empty response from ${providerName}`);
68
+ }
69
+
70
+ // Extract JSON from response (handles potential text around JSON)
71
+ const analysis = this.extractJson(aiText);
72
+ const elapsed = (Date.now() - startTime) / 1000;
73
+
74
+ logger.info('AI analysis complete', {
75
+ severity: analysis.severity,
76
+ elapsed: `${elapsed.toFixed(2)}s`,
77
+ model: this.model,
78
+ provider: providerName,
79
+ });
80
+
81
+ return {
82
+ ...analysis,
83
+ model: this.model,
84
+ provider: providerName,
85
+ response_time: parseFloat(elapsed.toFixed(2)),
86
+ };
87
+ } catch (error) {
88
+ const elapsed = (Date.now() - startTime) / 1000;
89
+ logger.error('AI analysis failed', {
90
+ error: error instanceof Error ? error.message : String(error),
91
+ elapsed: `${elapsed.toFixed(2)}s`,
92
+ provider: providerName,
93
+ });
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Extract JSON from AI response text
100
+ */
101
+ protected extractJson(text: string): RawAnalysisResult {
102
+ // Try to find JSON object in the response
103
+ const jsonMatch = text.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/);
104
+ if (!jsonMatch) {
105
+ throw new Error(`No JSON found in AI response: ${text.substring(0, 200)}`);
106
+ }
107
+
108
+ return JSON.parse(jsonMatch[0]) as RawAnalysisResult;
109
+ }
110
+
111
+ /**
112
+ * Build authorization headers if API key is set
113
+ */
114
+ protected getAuthHeaders(): Record<string, string> {
115
+ if (this.apiKey) {
116
+ return { Authorization: `Bearer ${this.apiKey}` };
117
+ }
118
+ return {};
119
+ }
120
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Ollama AI Provider
3
+ * Native Ollama API implementation
4
+ */
5
+
6
+ import type { AIProviderConfig, SensorData } from '../types.js';
7
+ import { buildSensorAnalysisPrompt } from '../prompts.js';
8
+ import { BaseAIProvider } from './base.js';
9
+
10
+ /**
11
+ * Ollama API response format
12
+ */
13
+ interface OllamaResponse {
14
+ response?: string;
15
+ error?: string;
16
+ }
17
+
18
+ /**
19
+ * Ollama AI provider using native Ollama API
20
+ */
21
+ export class OllamaProvider extends BaseAIProvider {
22
+ constructor(config: AIProviderConfig) {
23
+ super(config);
24
+ }
25
+
26
+ getProviderName(): string {
27
+ return 'ollama';
28
+ }
29
+
30
+ /**
31
+ * Make request to Ollama API
32
+ */
33
+ protected async makeRequest(sensors: SensorData): Promise<string> {
34
+ const prompt = buildSensorAnalysisPrompt(sensors);
35
+
36
+ const response = await fetch(`${this.baseUrl}/api/generate`, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ ...this.getAuthHeaders(),
41
+ },
42
+ body: JSON.stringify({
43
+ model: this.model,
44
+ prompt,
45
+ stream: false,
46
+ options: {
47
+ temperature: 0.3,
48
+ num_predict: 2000,
49
+ num_ctx: 4096,
50
+ },
51
+ }),
52
+ signal: AbortSignal.timeout(this.timeout),
53
+ });
54
+
55
+ if (!response.ok) {
56
+ const errorText = await response.text();
57
+ throw new Error(`Ollama request failed: ${response.status} ${errorText}`);
58
+ }
59
+
60
+ const result = (await response.json()) as OllamaResponse;
61
+ return result.response ?? '';
62
+ }
63
+
64
+ /**
65
+ * Check if Ollama is available
66
+ */
67
+ async checkHealth(): Promise<boolean> {
68
+ try {
69
+ const response = await fetch(`${this.baseUrl}/api/tags`, {
70
+ headers: this.getAuthHeaders(),
71
+ signal: AbortSignal.timeout(5000),
72
+ });
73
+ return response.ok;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * OpenAI-Compatible AI Provider
3
+ * Works with LocalAI, LM Studio, vLLM, and any OpenAI-compatible API
4
+ */
5
+
6
+ import type { AIProviderConfig, SensorData } from '../types.js';
7
+ import { buildChatMessages } from '../prompts.js';
8
+ import { BaseAIProvider } from './base.js';
9
+
10
+ /**
11
+ * OpenAI chat completion response format
12
+ */
13
+ interface OpenAIChatResponse {
14
+ choices?: Array<{
15
+ message?: {
16
+ content?: string;
17
+ };
18
+ }>;
19
+ error?: {
20
+ message?: string;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * OpenAI-compatible AI provider
26
+ * Works with LocalAI, LM Studio, vLLM, and other OpenAI-compatible APIs
27
+ */
28
+ export class OpenAICompatibleProvider extends BaseAIProvider {
29
+ constructor(config: AIProviderConfig) {
30
+ super(config);
31
+ }
32
+
33
+ getProviderName(): string {
34
+ return 'openai-compatible';
35
+ }
36
+
37
+ /**
38
+ * Make request to OpenAI-compatible API
39
+ */
40
+ protected async makeRequest(sensors: SensorData): Promise<string> {
41
+ const messages = buildChatMessages(sensors);
42
+
43
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...this.getAuthHeaders(),
48
+ },
49
+ body: JSON.stringify({
50
+ model: this.model,
51
+ messages,
52
+ temperature: 0.3,
53
+ max_tokens: 2000,
54
+ }),
55
+ signal: AbortSignal.timeout(this.timeout),
56
+ });
57
+
58
+ if (!response.ok) {
59
+ const errorText = await response.text();
60
+ throw new Error(`OpenAI-compatible API request failed: ${response.status} ${errorText}`);
61
+ }
62
+
63
+ const result = (await response.json()) as OpenAIChatResponse;
64
+
65
+ if (result.error) {
66
+ throw new Error(`API error: ${result.error.message}`);
67
+ }
68
+
69
+ return result.choices?.[0]?.message?.content ?? '';
70
+ }
71
+
72
+ /**
73
+ * Check if the OpenAI-compatible API is available
74
+ */
75
+ async checkHealth(): Promise<boolean> {
76
+ try {
77
+ const response = await fetch(`${this.baseUrl}/v1/models`, {
78
+ headers: this.getAuthHeaders(),
79
+ signal: AbortSignal.timeout(5000),
80
+ });
81
+ return response.ok;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Local AI Client Types
3
+ * Shared interfaces for AI provider implementations
4
+ */
5
+
6
+ /**
7
+ * Supported AI provider types
8
+ * Use 'none' to explicitly disable AI features
9
+ */
10
+ export type AIProviderType = 'ollama' | 'openai' | 'none';
11
+
12
+ /**
13
+ * Sensor data input for analysis
14
+ */
15
+ export interface SensorData {
16
+ [entity_id: string]: {
17
+ state: string;
18
+ unit?: string;
19
+ attributes?: Record<string, unknown>;
20
+ };
21
+ }
22
+
23
+ /**
24
+ * AI analysis result
25
+ */
26
+ export interface AnalysisResult {
27
+ severity: 'critical' | 'high' | 'medium' | 'low';
28
+ issue: string;
29
+ reasoning: string;
30
+ recommended_actions: string[];
31
+ ssh_diagnostics: string[];
32
+ confidence: number;
33
+ model: string;
34
+ provider: string;
35
+ response_time: number;
36
+ }
37
+
38
+ /**
39
+ * AI provider configuration
40
+ */
41
+ export interface AIProviderConfig {
42
+ provider: AIProviderType;
43
+ baseUrl: string;
44
+ model: string;
45
+ timeout: number;
46
+ apiKey?: string;
47
+ }
48
+
49
+ /**
50
+ * AI provider interface - all providers must implement this
51
+ */
52
+ export interface AIProviderInterface {
53
+ /**
54
+ * Analyze sensor data using AI
55
+ */
56
+ analyzeSensors(sensors: SensorData): Promise<AnalysisResult>;
57
+
58
+ /**
59
+ * Check if the AI provider is available
60
+ */
61
+ checkHealth(): Promise<boolean>;
62
+
63
+ /**
64
+ * Get the provider name
65
+ */
66
+ getProviderName(): string;
67
+ }