hybard-agent 0.1.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/lib/main.dart ADDED
@@ -0,0 +1,32 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() {
4
+ runApp(const MyApp());
5
+ }
6
+
7
+ class MyApp extends StatelessWidget {
8
+ const MyApp({super.key});
9
+
10
+ @override
11
+ Widget build(BuildContext context) {
12
+ return MaterialApp(
13
+ title: 'hybard-agent',
14
+ theme: ThemeData(primarySwatch: Colors.blue),
15
+ home: const HomeScreen(),
16
+ );
17
+ }
18
+ }
19
+
20
+ class HomeScreen extends StatelessWidget {
21
+ const HomeScreen({super.key});
22
+
23
+ @override
24
+ Widget build(BuildContext context) {
25
+ return Scaffold(
26
+ appBar: AppBar(title: const Text('Hybard Flutter App')),
27
+ body: const Center(
28
+ child: Text('Generated app for: flutter mobile app --port 7862'),
29
+ ),
30
+ );
31
+ }
32
+ }
package/lib/models.js ADDED
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Hybard Model Router — Multi-provider, multi-model AI routing.
3
+ *
4
+ * Supports:
5
+ * - Groq (free, fast — llama, mixtral, gemma)
6
+ * - Ollama (local — llama3.2, codellama, deepseek-coder, etc.)
7
+ * - OpenRouter (many models — Claude, Gemini, Llama, etc.)
8
+ * - OpenAI (GPT-4, GPT-3.5)
9
+ * - Custom endpoints (any OpenAI-compatible)
10
+ *
11
+ * Features:
12
+ * - Per-user model selection
13
+ * - Fallback chain (if one provider fails, try next)
14
+ * - Token counting & usage tracking
15
+ * - Model capabilities detection
16
+ *
17
+ * Usage:
18
+ * const router = new ModelRouter();
19
+ * const response = await router.chat('write fibonacci', { provider: 'groq' });
20
+ */
21
+
22
+ const https = require('https');
23
+ const http = require('http');
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ // ─── Provider Definitions ────────────────────────────────
28
+
29
+ const PROVIDERS = {
30
+ groq: {
31
+ name: 'Groq',
32
+ baseUrl: 'https://api.groq.com/openai/v1',
33
+ apiKey: process.env.GROQ_API_KEY || '',
34
+ models: {
35
+ 'llama-3.3-70b-versatile': { name: 'Llama 3.3 70B', size: '70B', coding: true, speed: 'fast' },
36
+ 'llama-3.1-8b-instant': { name: 'Llama 3.1 8B Instant', size: '8B', coding: true, speed: 'very-fast' },
37
+ 'mixtral-8x7b-32768': { name: 'Mixtral 8x7B', size: '8x7B', coding: true, speed: 'fast' },
38
+ 'gemma2-9b-it': { name: 'Gemma 2 9B', size: '9B', coding: false, speed: 'fast' },
39
+ },
40
+ defaultModel: 'llama-3.3-70b-versatile',
41
+ free: true,
42
+ },
43
+ ollama: {
44
+ name: 'Ollama (Local)',
45
+ baseUrl: 'http://localhost:11434',
46
+ apiKey: 'ollama',
47
+ models: {
48
+ 'llama3.2': { name: 'Llama 3.2', size: '3.2B', coding: true, speed: 'very-fast' },
49
+ 'codellama': { name: 'Code Llama', size: '7B', coding: true, speed: 'fast', codingSpecialist: true },
50
+ 'deepseek-coder': { name: 'DeepSeek Coder', size: '7B', coding: true, speed: 'fast', codingSpecialist: true },
51
+ 'llama3.1:8b': { name: 'Llama 3.1 8B', size: '8B', coding: true, speed: 'fast' },
52
+ 'qwen2.5-coder': { name: 'Qwen 2.5 Coder', size: '7B', coding: true, speed: 'fast', codingSpecialist: true },
53
+ 'phi3': { name: 'Phi-3', size: '3.8B', coding: true, speed: 'very-fast' },
54
+ 'mistral': { name: 'Mistral', size: '7B', coding: true, speed: 'fast' },
55
+ },
56
+ defaultModel: 'llama3.2',
57
+ free: true,
58
+ local: true,
59
+ },
60
+ openrouter: {
61
+ name: 'OpenRouter',
62
+ baseUrl: 'https://openrouter.ai/api/v1',
63
+ apiKey: process.env.OPENROUTER_API_KEY || '',
64
+ models: {
65
+ 'meta-llama/llama-3.3-70b-instruct': { name: 'Llama 3.3 70B', size: '70B', coding: true },
66
+ 'anthropic/claude-3.5-sonnet': { name: 'Claude 3.5 Sonnet', size: 'large', coding: true },
67
+ 'google/gemini-pro-1.5': { name: 'Gemini Pro 1.5', size: 'large', coding: true },
68
+ 'deepseek/deepseek-chat': { name: 'DeepSeek Chat', size: 'large', coding: true },
69
+ },
70
+ defaultModel: 'meta-llama/llama-3.3-70b-instruct',
71
+ free: false,
72
+ },
73
+ openai: {
74
+ name: 'OpenAI',
75
+ baseUrl: 'https://api.openai.com/v1',
76
+ apiKey: process.env.OPENAI_API_KEY || '',
77
+ models: {
78
+ 'gpt-4o': { name: 'GPT-4o', size: 'large', coding: true },
79
+ 'gpt-4o-mini': { name: 'GPT-4o Mini', size: 'medium', coding: true },
80
+ 'gpt-3.5-turbo': { name: 'GPT-3.5 Turbo', size: 'small', coding: true },
81
+ },
82
+ defaultModel: 'gpt-4o',
83
+ free: false,
84
+ },
85
+ };
86
+
87
+ // ─── Model Router Class ──────────────────────────────────
88
+
89
+ class ModelRouter {
90
+ constructor(options = {}) {
91
+ this.configDir = options.configDir || path.join(require('os').homedir(), '.hybard');
92
+ this.configFile = path.join(this.configDir, 'models.json');
93
+ this.config = this._loadConfig();
94
+ this.providers = { ...PROVIDERS };
95
+
96
+ // Load .env
97
+ this._loadEnv();
98
+ }
99
+
100
+ _loadEnv() {
101
+ const envPath = path.join(__dirname, '..', '.env');
102
+ if (fs.existsSync(envPath)) {
103
+ const envContent = fs.readFileSync(envPath, 'utf8');
104
+ for (const line of envContent.split('\n')) {
105
+ const trimmed = line.trim();
106
+ if (!trimmed || trimmed.startsWith('#')) continue;
107
+ const eqIdx = trimmed.indexOf('=');
108
+ if (eqIdx > 0) {
109
+ const key = trimmed.slice(0, eqIdx).trim();
110
+ const val = trimmed.slice(eqIdx + 1).trim();
111
+ if (!process.env[key]) process.env[key] = val;
112
+ }
113
+ }
114
+ // Update provider API keys from env
115
+ if (process.env.GROQ_API_KEY) this.providers.groq.apiKey = process.env.GROQ_API_KEY;
116
+ if (process.env.OPENROUTER_API_KEY) this.providers.openrouter.apiKey = process.env.OPENROUTER_API_KEY;
117
+ if (process.env.OPENAI_API_KEY) this.providers.openai.apiKey = process.env.OPENAI_API_KEY;
118
+ }
119
+ }
120
+
121
+ _loadConfig() {
122
+ try {
123
+ if (fs.existsSync(this.configFile)) {
124
+ return JSON.parse(fs.readFileSync(this.configFile, 'utf8'));
125
+ }
126
+ } catch (e) {}
127
+ return {
128
+ defaultProvider: 'groq',
129
+ defaultModel: 'llama-3.3-70b-versatile',
130
+ fallbackChain: ['groq', 'ollama'],
131
+ maxTokens: 2048,
132
+ temperature: 0.7,
133
+ };
134
+ }
135
+
136
+ saveConfig(updates = {}) {
137
+ this.config = { ...this.config, ...updates };
138
+ if (!fs.existsSync(this.configDir)) fs.mkdirSync(this.configDir, { recursive: true });
139
+ fs.writeFileSync(this.configFile, JSON.stringify(this.config, null, 2));
140
+ return this.config;
141
+ }
142
+
143
+ // ─── Provider Info ──────────────────────────────────
144
+
145
+ listProviders() {
146
+ return Object.entries(this.providers).map(([key, prov]) => ({
147
+ id: key,
148
+ name: prov.name,
149
+ free: prov.free || false,
150
+ local: prov.local || false,
151
+ hasApiKey: !!(prov.apiKey),
152
+ modelCount: Object.keys(prov.models).length,
153
+ isDefault: this.config.defaultProvider === key,
154
+ }));
155
+ }
156
+
157
+ listModels(provider = null) {
158
+ const results = [];
159
+ const providers = provider ? { [provider]: this.providers[provider] } : this.providers;
160
+
161
+ for (const [provId, prov] of Object.entries(providers)) {
162
+ if (!prov) continue;
163
+ for (const [modelId, model] of Object.entries(prov.models)) {
164
+ results.push({
165
+ provider: provId,
166
+ providerName: prov.name,
167
+ modelId,
168
+ ...model,
169
+ isDefault: this.config.defaultModel === modelId,
170
+ });
171
+ }
172
+ }
173
+ return results;
174
+ }
175
+
176
+ getCodingModels() {
177
+ return this.listModels().filter(m => m.coding);
178
+ }
179
+
180
+ getCodingSpecialists() {
181
+ return this.listModels().filter(m => m.codingSpecialist);
182
+ }
183
+
184
+ // ─── Chat ───────────────────────────────────────────
185
+
186
+ async chat(message, options = {}) {
187
+ const provider = options.provider || this.config.defaultProvider;
188
+ const model = options.model || this.config.defaultModel;
189
+ const prov = this.providers[provider];
190
+
191
+ if (!prov) throw new Error(`Unknown provider: ${provider}`);
192
+ if (!prov.apiKey && provider !== 'ollama') {
193
+ throw new Error(`No API key for ${provider}. Set it in .env or config.`);
194
+ }
195
+
196
+ // Try primary provider
197
+ try {
198
+ return await this._callProvider(provider, message, options);
199
+ } catch (err) {
200
+ // Fallback chain
201
+ const fallbacks = this.config.fallbackChain.filter(p => p !== provider);
202
+ for (const fb of fallbacks) {
203
+ try {
204
+ console.log(`[Fallback] ${provider} failed, trying ${fb}...`);
205
+ return await this._callProvider(fb, message, options);
206
+ } catch (e2) {
207
+ continue;
208
+ }
209
+ }
210
+ throw err;
211
+ }
212
+ }
213
+
214
+ async _callProvider(provider, message, options = {}) {
215
+ const prov = this.providers[provider];
216
+ const model = options.model || prov.defaultModel;
217
+
218
+ // Use native API for Ollama
219
+ if (provider === 'ollama') {
220
+ return this._callOllama(message, model, options);
221
+ }
222
+
223
+ // OpenAI-compatible API for others
224
+ return this._callOpenAICompatible(prov, model, message, options);
225
+ }
226
+
227
+ async _callOllama(message, model, options = {}) {
228
+ const body = JSON.stringify({
229
+ model,
230
+ prompt: message,
231
+ stream: false,
232
+ options: {
233
+ num_predict: options.maxTokens || this.config.maxTokens,
234
+ temperature: options.temperature ?? this.config.temperature,
235
+ },
236
+ });
237
+
238
+ return new Promise((resolve, reject) => {
239
+ const req = http.request(
240
+ {
241
+ hostname: 'localhost',
242
+ port: 11434,
243
+ path: '/api/generate',
244
+ method: 'POST',
245
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
246
+ timeout: 300000,
247
+ },
248
+ (res) => {
249
+ let data = '';
250
+ res.on('data', (chunk) => (data += chunk));
251
+ res.on('end', () => {
252
+ try {
253
+ const json = JSON.parse(data);
254
+ if (json.error) reject(new Error(json.error));
255
+ else resolve(json.response || '');
256
+ } catch (e) {
257
+ reject(new Error(`Ollama parse error: ${data.slice(0, 200)}`));
258
+ }
259
+ });
260
+ }
261
+ );
262
+ req.on('error', reject);
263
+ req.on('timeout', () => { req.destroy(); reject(new Error('Ollama timeout — model may be loading. Try again in a moment.')); });
264
+ req.write(body);
265
+ req.end();
266
+ });
267
+ }
268
+
269
+ async _callOpenAICompatible(prov, model, message, options = {}) {
270
+ const messages = [
271
+ { role: 'system', content: options.system || 'You are a coding assistant.' },
272
+ { role: 'user', content: message },
273
+ ];
274
+
275
+ const body = JSON.stringify({
276
+ model,
277
+ messages,
278
+ max_tokens: options.maxTokens || this.config.maxTokens,
279
+ temperature: options.temperature ?? this.config.temperature,
280
+ stream: false,
281
+ });
282
+
283
+ const baseUrl = new URL(prov.baseUrl);
284
+ const isHttps = baseUrl.protocol === 'https:';
285
+ const transport = isHttps ? https : http;
286
+
287
+ return new Promise((resolve, reject) => {
288
+ const req = transport.request(
289
+ {
290
+ hostname: baseUrl.hostname,
291
+ port: baseUrl.port || (isHttps ? 443 : 80),
292
+ path: `${baseUrl.pathname}/chat/completions`.replace(/\/+/g, '/'),
293
+ method: 'POST',
294
+ headers: {
295
+ 'Content-Type': 'application/json',
296
+ 'Authorization': `Bearer ${prov.apiKey}`,
297
+ 'Content-Length': Buffer.byteLength(body),
298
+ },
299
+ timeout: 60000,
300
+ },
301
+ (res) => {
302
+ let data = '';
303
+ res.on('data', (chunk) => (data += chunk));
304
+ res.on('end', () => {
305
+ try {
306
+ const json = JSON.parse(data);
307
+ if (json.error) reject(new Error(json.error.message || JSON.stringify(json.error)));
308
+ else resolve(json.choices?.[0]?.message?.content || '');
309
+ } catch (e) {
310
+ reject(new Error(`Parse error: ${data.slice(0, 200)}`));
311
+ }
312
+ });
313
+ }
314
+ );
315
+ req.on('error', reject);
316
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
317
+ req.write(body);
318
+ req.end();
319
+ });
320
+ }
321
+
322
+ // ─── Quick helpers ──────────────────────────────────
323
+
324
+ async generateCode(prompt, language = 'python', options = {}) {
325
+ const system = `You are an expert ${language} programmer. Generate clean, working code.
326
+ Respond ONLY with the code in a markdown code block. No explanations unless asked.`;
327
+ return this.chat(prompt, { ...options, system });
328
+ }
329
+
330
+ async explainCode(code, options = {}) {
331
+ const system = `You are a code analysis expert. Explain the given code clearly and concisely.
332
+ Cover: what it does, how it works, key patterns, and potential issues.`;
333
+ return this.chat(`Explain this code:\n\n\`\`\`\n${code}\n\`\`\``, { ...options, system });
334
+ }
335
+
336
+ async fixCode(code, error = '', options = {}) {
337
+ const system = `You are a debugging expert. Fix the given code.
338
+ Return ONLY the corrected code in a markdown code block.`;
339
+ const msg = error
340
+ ? `Fix this code. Error: ${error}\n\n\`\`\`\n${code}\n\`\`\``
341
+ : `Fix any issues in this code:\n\n\`\`\`\n${code}\n\`\`\``;
342
+ return this.chat(msg, { ...options, system });
343
+ }
344
+
345
+ async scaffold(goal, options = {}) {
346
+ const system = `You are a project scaffolding expert. Create project files for the given goal.
347
+ Return each file in this format:
348
+ ## filename.ext
349
+ \`\`\`language
350
+ file content here
351
+ \`\`\`
352
+ Include all necessary files (main code, config, tests, README).`;
353
+ return this.chat(`Create a project scaffold for: ${goal}`, { ...options, system });
354
+ }
355
+ }
356
+
357
+ module.exports = { ModelRouter, PROVIDERS };