future-lang 0.3.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.
- package/ARCHITECTURE.md +424 -0
- package/MIGRATION.md +365 -0
- package/README.md +370 -0
- package/ROADMAP.md +263 -0
- package/examples/adult.future +8 -0
- package/examples/api.future +11 -0
- package/examples/assistant.future +8 -0
- package/examples/browser-demo.html +164 -0
- package/examples/greet.future +7 -0
- package/examples/hello.future +1 -0
- package/examples/math.future +8 -0
- package/examples/mini-app.html +301 -0
- package/examples/smarthome.future +10 -0
- package/future-browser.js +102 -0
- package/future-playground.html +650 -0
- package/package.json +27 -0
- package/runtime/ai.js +92 -0
- package/runtime/browser.js +458 -0
- package/runtime/device.js +36 -0
- package/runtime/home.js +19 -0
- package/runtime/http.js +32 -0
- package/runtime/index.js +403 -0
- package/runtime/lsp-metadata.js +104 -0
- package/runtime/math.js +16 -0
- package/runtime/memory.js +61 -0
- package/runtime/mqtt.js +49 -0
- package/runtime/providers/anthropic.js +59 -0
- package/runtime/providers/index.js +93 -0
- package/runtime/providers/openai-compat.js +85 -0
- package/runtime/providers/util.js +70 -0
- package/runtime/rag/chunker.js +65 -0
- package/runtime/rag/pipeline.js +86 -0
- package/runtime/rag/vector-store.js +119 -0
- package/runtime/rag.js +94 -0
- package/runtime/schedule.js +77 -0
- package/runtime/system.js +101 -0
- package/runtime/tts.js +38 -0
- package/runtime/vision.js +85 -0
- package/server.js +42 -0
- package/src/ast.js +202 -0
- package/src/cli.js +391 -0
- package/src/errors.js +21 -0
- package/src/formatter.js +48 -0
- package/src/generator.js +457 -0
- package/src/index.js +48 -0
- package/src/lexer.js +248 -0
- package/src/parser.js +469 -0
package/runtime/ai.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// runtime/ai.js — Public AI module for Future programs.
|
|
2
|
+
//
|
|
3
|
+
// Provider resolution is delegated to runtime/providers/index.js.
|
|
4
|
+
// See that file for the full resolution order and supported providers.
|
|
5
|
+
//
|
|
6
|
+
// Quick-start:
|
|
7
|
+
// FUTURE_AI_PROVIDER=openai FUTURE_AI_API_KEY=sk-... future run my.future
|
|
8
|
+
// FUTURE_AI_PROVIDER=ollama FUTURE_AI_MODEL=llama3.2 future run my.future
|
|
9
|
+
// FUTURE_AI_PROVIDER=gemini FUTURE_AI_API_KEY=... future run my.future
|
|
10
|
+
//
|
|
11
|
+
// Or from Future code:
|
|
12
|
+
// ai.configure("https://api.venice.ai/api/v1", "my-key", "llama-3.3-70b")
|
|
13
|
+
|
|
14
|
+
import { resolveProvider, setRuntimeConfig } from './providers/index.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configure the AI provider from Future code.
|
|
18
|
+
* Accepts either a baseUrl (OpenAI-compat) or a named provider.
|
|
19
|
+
*
|
|
20
|
+
* Examples:
|
|
21
|
+
* ai.configure("https://api.venice.ai/api/v1", "key", "llama-3.3-70b")
|
|
22
|
+
* ai.configure("openai", "sk-...", "gpt-4o")
|
|
23
|
+
* ai.configure("anthropic", "sk-ant-...", "claude-sonnet-4-6")
|
|
24
|
+
*/
|
|
25
|
+
export function configure(baseUrlOrProvider, apiKey, model) {
|
|
26
|
+
const first = String(baseUrlOrProvider);
|
|
27
|
+
// If first arg looks like a URL, treat as OpenAI-compat baseUrl.
|
|
28
|
+
const isUrl = first.startsWith('http');
|
|
29
|
+
setRuntimeConfig({
|
|
30
|
+
provider: isUrl ? 'openai-compat' : first,
|
|
31
|
+
baseUrl: isUrl ? first : undefined,
|
|
32
|
+
apiKey: String(apiKey),
|
|
33
|
+
model: model ? String(model) : undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Ask a single question. @returns {Promise<string>} */
|
|
38
|
+
export async function ask(prompt) {
|
|
39
|
+
return chat([{ role: 'user', content: String(prompt) }]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Multi-turn chat. messages = [{ role, content }, ...]. @returns {Promise<string>} */
|
|
43
|
+
export async function chat(messages) {
|
|
44
|
+
const provider = resolveProvider();
|
|
45
|
+
if (!provider) return offlineStub(messages);
|
|
46
|
+
return provider.chat(messages);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stream a response chunk-by-chunk.
|
|
51
|
+
* @param {string|Array} promptOrMessages
|
|
52
|
+
* @param {(chunk: string) => void} onChunk Called with each text fragment.
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
export async function stream(promptOrMessages, onChunk) {
|
|
56
|
+
const messages = Array.isArray(promptOrMessages)
|
|
57
|
+
? promptOrMessages
|
|
58
|
+
: [{ role: 'user', content: String(promptOrMessages) }];
|
|
59
|
+
const provider = resolveProvider();
|
|
60
|
+
if (!provider) { onChunk(offlineStub(messages)); return; }
|
|
61
|
+
return provider.stream(messages, onChunk);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a vector embedding for a piece of text.
|
|
66
|
+
* With OpenAI/Ollama providers, returns real semantic embeddings.
|
|
67
|
+
* With Anthropic (no public embed API) or offline, returns keyword-based vectors.
|
|
68
|
+
* @param {string} text
|
|
69
|
+
* @returns {Promise<number[]>}
|
|
70
|
+
*/
|
|
71
|
+
export async function embed(text) {
|
|
72
|
+
const provider = resolveProvider();
|
|
73
|
+
if (!provider) {
|
|
74
|
+
const { keywordVector } = await import('./providers/util.js');
|
|
75
|
+
return keywordVector(String(text));
|
|
76
|
+
}
|
|
77
|
+
return provider.embed(text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- helpers ---
|
|
81
|
+
|
|
82
|
+
function offlineStub(messages) {
|
|
83
|
+
const preview = messages.map((m) => m.content).join(' ').slice(0, 80);
|
|
84
|
+
return (
|
|
85
|
+
'[ai offline] No provider configured.\n' +
|
|
86
|
+
' Option A — env: FUTURE_AI_PROVIDER=openai FUTURE_AI_API_KEY=sk-...\n' +
|
|
87
|
+
' env: FUTURE_AI_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-ant-...\n' +
|
|
88
|
+
' env: FUTURE_AI_PROVIDER=ollama (no key needed for local)\n' +
|
|
89
|
+
' Option B — code: ai.configure("openai", "sk-...", "gpt-4o")\n' +
|
|
90
|
+
` Prompt: ${preview}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// runtime/browser.js — Browser-compatible Future runtime.
|
|
2
|
+
// No Node.js dependencies. Designed to run in any modern browser.
|
|
3
|
+
//
|
|
4
|
+
// Modules fully supported: ai, http, rag, vision, memory, schedule, tts, device, math
|
|
5
|
+
// Modules partially supported: system (open + notify only), home (stub)
|
|
6
|
+
// Modules not available: mqtt (needs WebSocket broker), system.exec/read/write
|
|
7
|
+
// Built-ins: len() (sync), input() (window.prompt), print (overridable)
|
|
8
|
+
|
|
9
|
+
// ─── Shared utilities ─────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function keywordVector(text) {
|
|
12
|
+
const words = String(text).toLowerCase().match(/\b[a-z]{2,}\b/g) ?? [];
|
|
13
|
+
const vec = new Array(256).fill(0);
|
|
14
|
+
for (const word of words) {
|
|
15
|
+
let h = 5381;
|
|
16
|
+
for (let i = 0; i < word.length; i++) h = ((h << 5) + h) ^ word.charCodeAt(i);
|
|
17
|
+
vec[(h >>> 0) % 256] += 1;
|
|
18
|
+
}
|
|
19
|
+
const norm = Math.sqrt(vec.reduce((s, v) => s + v * v, 0)) || 1;
|
|
20
|
+
return vec.map((v) => v / norm);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cosineSim(a, b) {
|
|
24
|
+
let dot = 0, na = 0, nb = 0;
|
|
25
|
+
for (let i = 0; i < a.length; i++) { dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; }
|
|
26
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function* parseSSE(body) {
|
|
30
|
+
const reader = body.getReader();
|
|
31
|
+
const decoder = new TextDecoder();
|
|
32
|
+
let buf = '', pendingEvent = null;
|
|
33
|
+
while (true) {
|
|
34
|
+
const { done, value } = await reader.read();
|
|
35
|
+
if (done) break;
|
|
36
|
+
buf += decoder.decode(value, { stream: true });
|
|
37
|
+
const lines = buf.split('\n');
|
|
38
|
+
buf = lines.pop() ?? '';
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
if (line.startsWith('event: ')) { pendingEvent = line.slice(7).trim(); }
|
|
41
|
+
else if (line.startsWith('data: ')) {
|
|
42
|
+
const raw = line.slice(6).trim();
|
|
43
|
+
if (raw === '[DONE]') return;
|
|
44
|
+
try { yield { event: pendingEvent, data: JSON.parse(raw) }; } catch {}
|
|
45
|
+
pendingEvent = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Provider presets ─────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const PRESETS = {
|
|
54
|
+
anthropic: { baseUrl: 'https://api.anthropic.com', isAnthropic: true },
|
|
55
|
+
openai: { baseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4o-mini' },
|
|
56
|
+
gemini: { baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', defaultModel: 'gemini-2.0-flash' },
|
|
57
|
+
ollama: { baseUrl: 'http://localhost:11434/v1', defaultModel: 'llama3' },
|
|
58
|
+
openrouter: { baseUrl: 'https://openrouter.ai/api/v1', defaultModel: 'openai/gpt-4o-mini' },
|
|
59
|
+
venice: { baseUrl: 'https://api.venice.ai/api/v1', defaultModel: 'llama-3.3-70b' },
|
|
60
|
+
groq: { baseUrl: 'https://api.groq.com/openai/v1', defaultModel: 'llama-3.3-70b-versatile' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ─── AI module ────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const _aiState = { proxy: null, provider: null, apiKey: null, model: null };
|
|
66
|
+
|
|
67
|
+
function _resolveAI() { return _aiState; }
|
|
68
|
+
|
|
69
|
+
async function _proxyPost(path, body) {
|
|
70
|
+
const res = await fetch(`${_aiState.proxy}${path}`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) throw new Error(`Proxy error ${res.status}: ${await res.text()}`);
|
|
76
|
+
return res;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _extractText(data) {
|
|
80
|
+
// Handles both OpenAI and Anthropic response shapes
|
|
81
|
+
return data.text ?? data.content?.[0]?.text ??
|
|
82
|
+
data.choices?.[0]?.message?.content ?? String(data);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function _directAsk(messages) {
|
|
86
|
+
const preset = PRESETS[_aiState.provider ?? 'openai'] ?? PRESETS.openai;
|
|
87
|
+
const model = _aiState.model ?? preset.defaultModel ?? 'gpt-4o-mini';
|
|
88
|
+
|
|
89
|
+
if (preset.isAnthropic) {
|
|
90
|
+
const res = await fetch(`${preset.baseUrl}/v1/messages`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'x-api-key': _aiState.apiKey,
|
|
94
|
+
'anthropic-version': '2023-06-01',
|
|
95
|
+
'content-type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({ model, max_tokens: 1024, messages }),
|
|
98
|
+
});
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
return data.content?.[0]?.text ?? '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const res = await fetch(`${preset.baseUrl}/chat/completions`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Authorization': `Bearer ${_aiState.apiKey}`,
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({ model, messages }),
|
|
110
|
+
});
|
|
111
|
+
const data = await res.json();
|
|
112
|
+
return data.choices?.[0]?.message?.content ?? '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const ai = {
|
|
116
|
+
configure(providerOrUrl, apiKey, model) {
|
|
117
|
+
_aiState.provider = providerOrUrl;
|
|
118
|
+
_aiState.apiKey = apiKey ?? null;
|
|
119
|
+
_aiState.model = model ?? null;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async ask(prompt) {
|
|
123
|
+
if (_aiState.proxy) {
|
|
124
|
+
const res = await _proxyPost('/ask', { prompt: String(prompt) });
|
|
125
|
+
const data = await res.json();
|
|
126
|
+
return _extractText(data);
|
|
127
|
+
}
|
|
128
|
+
if (!_aiState.apiKey) throw new Error('AI not configured. Call Future.configure({ proxy: "/api/ai" }) or Future.configure({ provider: "openai", apiKey: "sk-..." })');
|
|
129
|
+
return _directAsk([{ role: 'user', content: String(prompt) }]);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
async chat(messages) {
|
|
133
|
+
if (_aiState.proxy) {
|
|
134
|
+
const res = await _proxyPost('/chat', { messages });
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
return _extractText(data);
|
|
137
|
+
}
|
|
138
|
+
if (!_aiState.apiKey) throw new Error('AI not configured. Call Future.configure({ proxy: "/api/ai" }) or Future.configure({ provider: "openai", apiKey: "sk-..." })');
|
|
139
|
+
return _directAsk(messages);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async stream(prompt, onChunk) {
|
|
143
|
+
if (_aiState.proxy) {
|
|
144
|
+
const res = await _proxyPost('/stream', { prompt: String(prompt) });
|
|
145
|
+
for await (const { data } of parseSSE(res.body)) {
|
|
146
|
+
const chunk = data.choices?.[0]?.delta?.content ?? data.delta?.text ?? '';
|
|
147
|
+
if (chunk) await onChunk(chunk);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Direct streaming (demo mode)
|
|
152
|
+
const preset = PRESETS[_aiState.provider ?? 'openai'] ?? PRESETS.openai;
|
|
153
|
+
const model = _aiState.model ?? preset.defaultModel ?? 'gpt-4o-mini';
|
|
154
|
+
const res = await fetch(`${preset.baseUrl}/chat/completions`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Authorization': `Bearer ${_aiState.apiKey}`, 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ model, stream: true, messages: [{ role: 'user', content: String(prompt) }] }),
|
|
158
|
+
});
|
|
159
|
+
for await (const { data } of parseSSE(res.body)) {
|
|
160
|
+
const chunk = data.choices?.[0]?.delta?.content ?? '';
|
|
161
|
+
if (chunk) await onChunk(chunk);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async embed(text) {
|
|
166
|
+
if (_aiState.proxy) {
|
|
167
|
+
const res = await _proxyPost('/embed', { text: String(text) });
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
return data.embedding ?? data.data?.[0]?.embedding ?? keywordVector(text);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const preset = PRESETS[_aiState.provider ?? 'openai'] ?? PRESETS.openai;
|
|
173
|
+
const res = await fetch(`${preset.baseUrl}/embeddings`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Authorization': `Bearer ${_aiState.apiKey}`, 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({ model: 'text-embedding-3-small', input: String(text) }),
|
|
177
|
+
});
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
return data.data?.[0]?.embedding ?? keywordVector(text);
|
|
180
|
+
} catch {
|
|
181
|
+
return keywordVector(text);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// ─── HTTP module ──────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export const http = {
|
|
189
|
+
async get(url, headers = {}) {
|
|
190
|
+
const res = await fetch(String(url), { headers });
|
|
191
|
+
return res.headers.get('content-type')?.includes('json') ? res.json() : res.text();
|
|
192
|
+
},
|
|
193
|
+
async post(url, body, headers = {}) {
|
|
194
|
+
const res = await fetch(String(url), {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
return res.headers.get('content-type')?.includes('json') ? res.json() : res.text();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ─── Memory module ────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
const _store = new Map();
|
|
206
|
+
|
|
207
|
+
export const memory = {
|
|
208
|
+
set(key, value) { _store.set(String(key), value); return value; },
|
|
209
|
+
get(key) { return _store.has(String(key)) ? _store.get(String(key)) : null; },
|
|
210
|
+
delete(key) { return _store.delete(String(key)); },
|
|
211
|
+
search(query) {
|
|
212
|
+
const q = String(query).toLowerCase();
|
|
213
|
+
return [..._store.entries()]
|
|
214
|
+
.filter(([k, v]) => k.toLowerCase().includes(q) || String(v).toLowerCase().includes(q))
|
|
215
|
+
.map(([key, value]) => ({ key, value }));
|
|
216
|
+
},
|
|
217
|
+
forget(pattern) {
|
|
218
|
+
if (!pattern) { const n = _store.size; _store.clear(); return n; }
|
|
219
|
+
let n = 0;
|
|
220
|
+
const re = pattern instanceof RegExp ? pattern : null;
|
|
221
|
+
for (const key of [..._store.keys()]) {
|
|
222
|
+
if (re ? re.test(key) : key.includes(String(pattern))) { _store.delete(key); n++; }
|
|
223
|
+
}
|
|
224
|
+
return n;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// ─── Schedule module ──────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function parseDuration(d) {
|
|
231
|
+
if (typeof d === 'number') return d;
|
|
232
|
+
const s = String(d);
|
|
233
|
+
if (s.endsWith('ms')) return Number(s.slice(0, -2));
|
|
234
|
+
if (s.endsWith('s')) return Number(s.slice(0, -1)) * 1000;
|
|
235
|
+
if (s.endsWith('m')) return Number(s.slice(0, -1)) * 60000;
|
|
236
|
+
if (s.endsWith('h')) return Number(s.slice(0, -1)) * 3600000;
|
|
237
|
+
return Number(s);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const schedule = {
|
|
241
|
+
async every(interval, callback) {
|
|
242
|
+
const ms = parseDuration(interval);
|
|
243
|
+
return setInterval(async () => { try { await callback(); } catch (e) { console.error('[schedule]', e); } }, ms);
|
|
244
|
+
},
|
|
245
|
+
async once(delay, callback) {
|
|
246
|
+
const ms = parseDuration(delay);
|
|
247
|
+
return setTimeout(async () => { try { await callback(); } catch (e) { console.error('[schedule]', e); } }, ms);
|
|
248
|
+
},
|
|
249
|
+
async cron() { console.warn('[schedule.cron] not available in browser'); },
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// ─── TTS module ───────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export const tts = {
|
|
255
|
+
async speak(text) {
|
|
256
|
+
if (!('speechSynthesis' in window)) { console.warn('[tts] Web Speech API not available'); return; }
|
|
257
|
+
const utt = new SpeechSynthesisUtterance(String(text));
|
|
258
|
+
speechSynthesis.speak(utt);
|
|
259
|
+
return String(text);
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// ─── System module (browser subset) ──────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export const system = {
|
|
266
|
+
async open(target) { window.open(String(target), '_blank'); return String(target); },
|
|
267
|
+
async notify(message) {
|
|
268
|
+
const msg = String(message);
|
|
269
|
+
if ('Notification' in window) {
|
|
270
|
+
if (Notification.permission === 'granted') { new Notification('Future', { body: msg }); }
|
|
271
|
+
else if (Notification.permission !== 'denied') {
|
|
272
|
+
const p = await Notification.requestPermission();
|
|
273
|
+
if (p === 'granted') new Notification('Future', { body: msg });
|
|
274
|
+
}
|
|
275
|
+
} else { console.log(`[notify] ${msg}`); }
|
|
276
|
+
return msg;
|
|
277
|
+
},
|
|
278
|
+
async exec() { throw new Error('system.exec is not available in the browser'); },
|
|
279
|
+
async read() { throw new Error('system.read is not available in the browser'); },
|
|
280
|
+
async write() { throw new Error('system.write is not available in the browser'); },
|
|
281
|
+
env(name) { return window.__env?.[String(name)] ?? null; },
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ─── Vision module ────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
function visionMsg(prompt, image) {
|
|
287
|
+
return [{ role: 'user', content: [
|
|
288
|
+
{ type: 'text', text: prompt },
|
|
289
|
+
{ type: 'image_url', image_url: { url: String(image) } },
|
|
290
|
+
]}];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const vision = {
|
|
294
|
+
async describe(image) { return ai.chat(visionMsg('Describe this image in detail.', image)); },
|
|
295
|
+
async detect(image) { return ai.chat(visionMsg('List all objects, people and elements visible in this image.', image)); },
|
|
296
|
+
async ocr(image) { return ai.chat(visionMsg('Extract all text from this image. Return only the text.', image)); },
|
|
297
|
+
async classify(image) { return ai.chat(visionMsg('Classify this image into a primary category in 1-2 words.', image)); },
|
|
298
|
+
async compare(a, b) {
|
|
299
|
+
return ai.chat([{ role: 'user', content: [
|
|
300
|
+
{ type: 'text', text: 'Compare these two images and describe their differences.' },
|
|
301
|
+
{ type: 'image_url', image_url: { url: String(a) } },
|
|
302
|
+
{ type: 'image_url', image_url: { url: String(b) } },
|
|
303
|
+
]}]);
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// ─── RAG module ───────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
function chunk(text, size = 512, overlap = 64) {
|
|
310
|
+
const sentences = String(text).match(/[^.!?]+[.!?]*/g) ?? [String(text)];
|
|
311
|
+
const chunks = [];
|
|
312
|
+
let cur = '';
|
|
313
|
+
for (const s of sentences) {
|
|
314
|
+
if (cur.length + s.length > size && cur) {
|
|
315
|
+
chunks.push(cur.trim());
|
|
316
|
+
cur = cur.slice(-overlap);
|
|
317
|
+
}
|
|
318
|
+
cur += s;
|
|
319
|
+
}
|
|
320
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
321
|
+
return chunks;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createMemoryStore() {
|
|
325
|
+
const entries = [];
|
|
326
|
+
return {
|
|
327
|
+
async add(text, vector, meta = {}) { entries.push({ text, vector, meta }); },
|
|
328
|
+
async search(queryVec, k = 3) {
|
|
329
|
+
return entries
|
|
330
|
+
.map((e) => ({ ...e, score: cosineSim(queryVec, e.vector) }))
|
|
331
|
+
.sort((a, b) => b.score - a.score)
|
|
332
|
+
.slice(0, k);
|
|
333
|
+
},
|
|
334
|
+
stats() { return { chunks: entries.length }; },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function createPipeline(name) {
|
|
339
|
+
const store = createMemoryStore();
|
|
340
|
+
return {
|
|
341
|
+
name,
|
|
342
|
+
async index(docs) {
|
|
343
|
+
const list = Array.isArray(docs) ? docs : [docs];
|
|
344
|
+
let n = 0;
|
|
345
|
+
for (const doc of list) {
|
|
346
|
+
const text = typeof doc === 'string' ? doc : doc.text ?? doc.content ?? JSON.stringify(doc);
|
|
347
|
+
const source = doc.source ?? name;
|
|
348
|
+
for (const [i, c] of chunk(text).entries()) {
|
|
349
|
+
const vec = await ai.embed(c);
|
|
350
|
+
await store.add(c, vec, { source, chunkIndex: i });
|
|
351
|
+
n++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { indexed: n };
|
|
355
|
+
},
|
|
356
|
+
async query(question) {
|
|
357
|
+
const vec = await ai.embed(String(question));
|
|
358
|
+
const results = await store.search(vec);
|
|
359
|
+
if (!results.length) return 'No relevant information found.';
|
|
360
|
+
const context = results.map((r) => r.text).join('\n\n');
|
|
361
|
+
return ai.ask(`Context:\n${context}\n\nQuestion: ${question}\n\nAnswer based on the context:`);
|
|
362
|
+
},
|
|
363
|
+
stats() { return store.stats(); },
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const _defaultPipeline = createPipeline('default');
|
|
368
|
+
|
|
369
|
+
export const rag = {
|
|
370
|
+
async index(docs) { return _defaultPipeline.index(Array.isArray(docs) ? docs : [docs]); },
|
|
371
|
+
async query(q) { return _defaultPipeline.query(String(q)); },
|
|
372
|
+
create(name) { return createPipeline(String(name)); },
|
|
373
|
+
async indexFile() { throw new Error('rag.indexFile is not available in the browser. Use rag.index() with text content.'); },
|
|
374
|
+
async indexUrl(url) {
|
|
375
|
+
const res = await fetch(String(url));
|
|
376
|
+
const text = await res.text();
|
|
377
|
+
const clean = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
378
|
+
return _defaultPipeline.index([{ text: clean, source: String(url) }]);
|
|
379
|
+
},
|
|
380
|
+
stats() { return _defaultPipeline.stats(); },
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// ─── Device module ────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
const _devices = new Map();
|
|
386
|
+
|
|
387
|
+
export const device = {
|
|
388
|
+
register(config) {
|
|
389
|
+
if (!config?.name) throw new Error('device.register requires a config object with a name');
|
|
390
|
+
const d = { ...config, registeredAt: Date.now() };
|
|
391
|
+
_devices.set(config.name, d);
|
|
392
|
+
return d;
|
|
393
|
+
},
|
|
394
|
+
get(name) { return _devices.get(String(name)) ?? null; },
|
|
395
|
+
list() { return [..._devices.values()]; },
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// ─── MQTT stub ────────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
export const mqtt = {
|
|
401
|
+
async publish() { console.warn('[mqtt] not available in browser without a WebSocket broker'); },
|
|
402
|
+
async subscribe() { console.warn('[mqtt] not available in browser without a WebSocket broker'); },
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// ─── Home stub ────────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
export const home = {
|
|
408
|
+
async turnOn(d) { return mqtt.publish(`home/${d}/set`, 'ON'); },
|
|
409
|
+
async turnOff(d) { return mqtt.publish(`home/${d}/set`, 'OFF'); },
|
|
410
|
+
async set(d, value) { return mqtt.publish(`home/${d}/set`, String(value)); },
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// ─── Math module ─────────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
export const math = {
|
|
416
|
+
round: (x) => Math.round(Number(x)),
|
|
417
|
+
floor: (x) => Math.floor(Number(x)),
|
|
418
|
+
ceil: (x) => Math.ceil(Number(x)),
|
|
419
|
+
abs: (x) => Math.abs(Number(x)),
|
|
420
|
+
sqrt: (x) => Math.sqrt(Number(x)),
|
|
421
|
+
pow: (x, y) => Math.pow(Number(x), Number(y)),
|
|
422
|
+
log: (x) => Math.log(Number(x)),
|
|
423
|
+
random: () => Math.random(),
|
|
424
|
+
min: (...args) => Math.min(...args.map(Number)),
|
|
425
|
+
max: (...args) => Math.max(...args.map(Number)),
|
|
426
|
+
pi: Math.PI,
|
|
427
|
+
e: Math.E,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// ─── input() — browser version uses window.prompt ────────────────────────────
|
|
431
|
+
|
|
432
|
+
export async function input(prompt = '') {
|
|
433
|
+
return window.prompt(String(prompt)) ?? '';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ─── Print function (overridable) ─────────────────────────────────────────────
|
|
437
|
+
// In browser mode the generator emits `__rt.print(...)` instead of `console.log(...)`.
|
|
438
|
+
// Override this to redirect output to the DOM, a custom logger, etc.
|
|
439
|
+
//
|
|
440
|
+
// Example:
|
|
441
|
+
// Future.runtime.print = (...args) => {
|
|
442
|
+
// document.getElementById('output').textContent += args.join(' ') + '\n';
|
|
443
|
+
// };
|
|
444
|
+
|
|
445
|
+
export function print(...args) {
|
|
446
|
+
console.log(...args);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─── Combined runtime object ──────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
export const browserRuntime = { ai, http, memory, schedule, tts, system, vision, rag, device, mqtt, home, math, input, print };
|
|
452
|
+
|
|
453
|
+
// ─── Global proxy configuration ───────────────────────────────────────────────
|
|
454
|
+
// Called by: Future.configure({ proxy: '/api/ai' })
|
|
455
|
+
|
|
456
|
+
export function setProxy(proxyUrl) {
|
|
457
|
+
_aiState.proxy = proxyUrl;
|
|
458
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// runtime/device.js — IoT device registry.
|
|
2
|
+
// Devices are stored in-process. For persistence, replace `registry` with a
|
|
3
|
+
// call to Home Assistant / AWS IoT / Azure IoT Hub, or write to disk.
|
|
4
|
+
|
|
5
|
+
const registry = new Map();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register a device. `config` must have at least a `name` field.
|
|
9
|
+
* @param {{ name: string, type?: string, location?: string, [key: string]: any }} config
|
|
10
|
+
* @returns {object} the registered device record.
|
|
11
|
+
*/
|
|
12
|
+
export function register(config) {
|
|
13
|
+
if (!config || !config.name) {
|
|
14
|
+
throw new Error('device.register: config.name is required');
|
|
15
|
+
}
|
|
16
|
+
const record = { ...config, name: String(config.name), registeredAt: Date.now() };
|
|
17
|
+
registry.set(record.name, record);
|
|
18
|
+
return record;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Look up a registered device by name.
|
|
23
|
+
* @param {string} name
|
|
24
|
+
* @returns {object|null}
|
|
25
|
+
*/
|
|
26
|
+
export function get(name) {
|
|
27
|
+
return registry.get(String(name)) ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* List all registered devices.
|
|
32
|
+
* @returns {object[]}
|
|
33
|
+
*/
|
|
34
|
+
export function list() {
|
|
35
|
+
return [...registry.values()];
|
|
36
|
+
}
|
package/runtime/home.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// runtime/home.js — Home Automation (extension point).
|
|
2
|
+
// Demonstrates composition: it drives devices over MQTT. Re-target to Home
|
|
3
|
+
// Assistant, Hue, Matter, etc. by changing only this file.
|
|
4
|
+
|
|
5
|
+
import * as mqtt from './mqtt.js';
|
|
6
|
+
|
|
7
|
+
async function setState(device, value) {
|
|
8
|
+
await mqtt.publish(`home/${device}/set`, String(value));
|
|
9
|
+
return `${device} -> ${value}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Turn a device on. @returns {Promise<string>} */
|
|
13
|
+
export async function turnOn(device) { return setState(device, 'on'); }
|
|
14
|
+
|
|
15
|
+
/** Turn a device off. @returns {Promise<string>} */
|
|
16
|
+
export async function turnOff(device) { return setState(device, 'off'); }
|
|
17
|
+
|
|
18
|
+
/** Set a device to an arbitrary value. @returns {Promise<string>} */
|
|
19
|
+
export async function set(device, value) { return setState(device, value); }
|
package/runtime/http.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// runtime/http.js — consume REST APIs.
|
|
2
|
+
// Uses the global fetch (stable in Node 22). Returns parsed JSON or text.
|
|
3
|
+
|
|
4
|
+
// Default headers — many public APIs (e.g. GitHub) reject requests without a
|
|
5
|
+
// User-Agent. Callers can override any of these.
|
|
6
|
+
const DEFAULT_HEADERS = {
|
|
7
|
+
'user-agent': 'future-lang/0.2 (+https://github.com/future-lang)',
|
|
8
|
+
accept: 'application/json, text/*;q=0.9, */*;q=0.8',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function parse(res) {
|
|
12
|
+
const ct = res.headers.get('content-type') || '';
|
|
13
|
+
return ct.includes('application/json') ? res.json() : res.text();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** GET a URL. @returns parsed JSON object/array, or text. */
|
|
17
|
+
export async function get(url, headers = {}) {
|
|
18
|
+
const res = await fetch(url, { headers: { ...DEFAULT_HEADERS, ...headers } });
|
|
19
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
|
|
20
|
+
return parse(res);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** POST a JSON body to a URL. @returns parsed JSON or text. */
|
|
24
|
+
export async function post(url, body, headers = {}) {
|
|
25
|
+
const res = await fetch(url, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { ...DEFAULT_HEADERS, 'content-type': 'application/json', ...headers },
|
|
28
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
|
|
31
|
+
return parse(res);
|
|
32
|
+
}
|