bimmo-cli 1.2.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/.bimmo-context.md +26 -0
- package/README.md +67 -0
- package/bin/bimmo +19 -0
- package/package.json +50 -0
- package/src/agent.js +110 -0
- package/src/config.js +151 -0
- package/src/interface.js +271 -0
- package/src/project-context.js +40 -0
- package/src/providers/anthropic.js +77 -0
- package/src/providers/base.js +9 -0
- package/src/providers/factory.js +20 -0
- package/src/providers/gemini.js +77 -0
- package/src/providers/grok.js +47 -0
- package/src/providers/ollama.js +51 -0
- package/src/providers/openai.js +77 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# BIMMO-CLI: Protocolo de Operação e Contexto
|
|
2
|
+
|
|
3
|
+
Você é o **bimmo**, uma IA universal operando via terminal com uma interface verde e lavanda. Seu objetivo é auxiliar o usuário em tarefas de codificação, pesquisa e automação com máxima precisão.
|
|
4
|
+
|
|
5
|
+
## 🛡️ Diretrizes de Comportamento
|
|
6
|
+
1. **Identidade:** Você é o bimmo. Mantenha um tom profissional, técnico e direto.
|
|
7
|
+
2. **Modos de Operação:**
|
|
8
|
+
- **CHAT:** Apenas conversa. Não altere arquivos.
|
|
9
|
+
- **PLAN:** Analise e descreva mudanças. Não as execute.
|
|
10
|
+
- **EDIT:** Você tem permissão para usar `write_file` e `run_command`. Aplique as mudanças solicitadas com precisão cirúrgica.
|
|
11
|
+
3. **Padrões de Código:** Siga sempre as convenções existentes no projeto do usuário. Prefira código limpo, tipado (quando aplicável) e bem documentado.
|
|
12
|
+
|
|
13
|
+
## 🛠️ Ferramentas (Tools)
|
|
14
|
+
Você tem acesso a:
|
|
15
|
+
- `search_internet`: Use para buscar informações atualizadas.
|
|
16
|
+
- `read_file`: Use para ler o contexto de arquivos locais.
|
|
17
|
+
- `write_file`: Use para criar ou modificar arquivos no modo EDIT.
|
|
18
|
+
- `run_command`: Use para executar comandos shell (build, tests, install).
|
|
19
|
+
|
|
20
|
+
## 📂 Contexto do Projeto
|
|
21
|
+
Sempre que o usuário utilizar `@caminho/do/arquivo`, o conteúdo desse arquivo será anexado à mensagem dele. Use isso para entender a estrutura e a lógica antes de sugerir ou aplicar mudanças.
|
|
22
|
+
|
|
23
|
+
## ⚠️ Regras Críticas
|
|
24
|
+
- Nunca apague arquivos sem confirmação explícita.
|
|
25
|
+
- No modo EDIT, valide se o código gerado é funcional.
|
|
26
|
+
- Se não tiver certeza de algo, peça clarificação usando o modo CHAT ou PLAN.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 🌿 bimmo-cli
|
|
2
|
+
|
|
3
|
+
**bimmo** é um assistente de IA universal para o seu terminal, com uma interface elegante em tons de **Verde & Lavanda**. Ele funciona como um agente autônomo e multimodal que gerencia o contexto do seu projeto e executa tarefas complexas.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📦 Instalação Global
|
|
8
|
+
|
|
9
|
+
Para ter o **bimmo** sempre disponível no seu terminal como um comando do sistema:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g bimmo-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Após a instalação, basta digitar `bimmo` em qualquer pasta:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bimmo
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## ✨ Principais Funcionalidades
|
|
24
|
+
|
|
25
|
+
### 🤖 Agente Autônomo e Modos de Operação
|
|
26
|
+
- **/chat**: Conversa normal para tirar dúvidas.
|
|
27
|
+
- **/plan**: Analisa o código e descreve o plano de ação sem alterar nada.
|
|
28
|
+
- **/edit**: **Modo Auto-Edit**. A IA tem permissão para ler, criar, editar arquivos e rodar comandos shell para resolver problemas.
|
|
29
|
+
|
|
30
|
+
### 📁 Contexto Inteligente de Projeto
|
|
31
|
+
- **Auto-Indexação**: O bimmo mapeia a estrutura do seu projeto na inicialização.
|
|
32
|
+
- **Herança de Regras**: Detecta automaticamente arquivos `.bimmorc.json`, `CLAUDE.md` e `INSTRUCTIONS.md`.
|
|
33
|
+
- **Anexos com `@`**: Digite `@caminho/arquivo` para anexar códigos ou imagens à conversa.
|
|
34
|
+
|
|
35
|
+
### 🌐 Busca e Multi-Provedor
|
|
36
|
+
- **Busca na Web**: Integrado com Tavily API para pesquisas em tempo real.
|
|
37
|
+
- **Perfis**: Salve múltiplos perfis (OpenAI, Anthropic, Gemini, DeepSeek, Grok, Ollama, OpenRouter, Z.ai) e alterne instantaneamente com `/switch`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🛠️ Comandos Rápidos no Chat
|
|
42
|
+
|
|
43
|
+
| Comando | Função |
|
|
44
|
+
| :--- | :--- |
|
|
45
|
+
| `/chat` | Modo conversa. |
|
|
46
|
+
| `/plan` | Modo planejamento (seguro). |
|
|
47
|
+
| `/edit` | Modo edição automática (agente). |
|
|
48
|
+
| `/init` | Cria o arquivo `.bimmorc.json` no projeto. |
|
|
49
|
+
| `/switch [nome]` | Troca de perfil (IA/Chave/Modelo). |
|
|
50
|
+
| `/model [nome]` | Troca apenas o modelo da IA atual. |
|
|
51
|
+
| `/config` | Gerenciar seus perfis e chaves de API. |
|
|
52
|
+
| `@arquivo` | Lê um arquivo ou imagem para o contexto. |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚀 Publicando via GitHub (Automação)
|
|
57
|
+
|
|
58
|
+
Para publicar novas versões no NPM automaticamente usando o GitHub:
|
|
59
|
+
|
|
60
|
+
1. Suba seu código para um repositório no GitHub.
|
|
61
|
+
2. No NPM, gere um **Access Token (Automation)**.
|
|
62
|
+
3. No GitHub, vá em **Settings > Secrets > Actions** e adicione o secret `NPM_TOKEN`.
|
|
63
|
+
4. Sempre que você criar uma **"New Release"** no GitHub, o projeto será publicado no NPM e ficará disponível para `npm install -g`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
Feito com 💜 por Judah.
|
package/bin/bimmo
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { startInteractive } from '../src/interface.js';
|
|
5
|
+
import { configure } from '../src/config.js';
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('bimmo')
|
|
9
|
+
.description('bimmo — Sua IA universal no terminal (verde & lavanda)')
|
|
10
|
+
.version('1.1.0');
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.command('config')
|
|
14
|
+
.description('Configurar provedor e chave de API')
|
|
15
|
+
.action(configure);
|
|
16
|
+
|
|
17
|
+
program.action(startInteractive); // ao digitar só "bimmo" entra no chat
|
|
18
|
+
|
|
19
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bimmo-cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "🌿 CLI universal para IAs com interface verde/lavanda, modo agente (Auto-Edit) e contexto inteligente de projeto.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bimmo": "./bin/bimmo"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"ai",
|
|
11
|
+
"cli",
|
|
12
|
+
"openai",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"gemini",
|
|
15
|
+
"grok",
|
|
16
|
+
"deepseek",
|
|
17
|
+
"openrouter",
|
|
18
|
+
"agent",
|
|
19
|
+
"multimodal",
|
|
20
|
+
"terminal"
|
|
21
|
+
],
|
|
22
|
+
"author": "Judah",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"files": [
|
|
25
|
+
"bin/",
|
|
26
|
+
"src/",
|
|
27
|
+
"README.md",
|
|
28
|
+
".bimmo-context.md"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@anthropic-ai/sdk": "^0.36.3",
|
|
35
|
+
"@google/generative-ai": "^0.21.0",
|
|
36
|
+
"@tavily/core": "^0.0.2",
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"conf": "^13.0.0",
|
|
40
|
+
"figlet": "^1.7.0",
|
|
41
|
+
"inquirer": "^10.1.0",
|
|
42
|
+
"marked": "^14.0.0",
|
|
43
|
+
"marked-terminal": "^7.0.0",
|
|
44
|
+
"mime-types": "^2.1.35",
|
|
45
|
+
"ollama": "^0.5.12",
|
|
46
|
+
"openai": "^4.82.0",
|
|
47
|
+
"ora": "^8.1.1",
|
|
48
|
+
"zod": "^3.24.1"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { tavily } from '@tavily/core';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getConfig } from './config.js';
|
|
6
|
+
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
const tvly = config.tavilyKey ? tavily({ apiKey: config.tavilyKey }) : null;
|
|
9
|
+
|
|
10
|
+
export const tools = [
|
|
11
|
+
{
|
|
12
|
+
name: 'search_internet',
|
|
13
|
+
description: 'Pesquisa informações atualizadas na internet sobre qualquer assunto.',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
query: { type: 'string', description: 'O termo de busca' }
|
|
18
|
+
},
|
|
19
|
+
required: ['query']
|
|
20
|
+
},
|
|
21
|
+
execute: async ({ query }) => {
|
|
22
|
+
if (!tvly) return 'Erro: Chave de API da Tavily não configurada. Use /config para configurar.';
|
|
23
|
+
const searchResponse = await tvly.search(query, {
|
|
24
|
+
searchDepth: 'advanced',
|
|
25
|
+
maxResults: 5
|
|
26
|
+
});
|
|
27
|
+
return JSON.stringify(searchResponse.results.map(r => ({
|
|
28
|
+
title: r.title,
|
|
29
|
+
url: r.url,
|
|
30
|
+
content: r.content
|
|
31
|
+
})));
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'read_file',
|
|
36
|
+
description: 'Lê o conteúdo de um arquivo no sistema.',
|
|
37
|
+
parameters: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: 'string', description: 'Caminho do arquivo' }
|
|
41
|
+
},
|
|
42
|
+
required: ['path']
|
|
43
|
+
},
|
|
44
|
+
execute: async ({ path: filePath }) => {
|
|
45
|
+
try {
|
|
46
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return `Erro ao ler arquivo: ${err.message}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'write_file',
|
|
54
|
+
description: 'Cria ou sobrescreve um arquivo com um conteúdo específico.',
|
|
55
|
+
parameters: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
path: { type: 'string', description: 'Camin de destino' },
|
|
59
|
+
content: { type: 'string', description: 'Conteúdo do arquivo' }
|
|
60
|
+
},
|
|
61
|
+
required: ['path', 'content']
|
|
62
|
+
},
|
|
63
|
+
execute: async ({ path: filePath, content }) => {
|
|
64
|
+
try {
|
|
65
|
+
const dir = path.dirname(filePath);
|
|
66
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(filePath, content);
|
|
68
|
+
return `Arquivo ${filePath} criado com sucesso.`;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return `Erro ao escrever arquivo: ${err.message}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'run_command',
|
|
76
|
+
description: 'Executa um comando shell no sistema.',
|
|
77
|
+
parameters: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
command: { type: 'string', description: 'Comando shell a ser executado' }
|
|
81
|
+
},
|
|
82
|
+
required: ['command']
|
|
83
|
+
},
|
|
84
|
+
execute: async ({ command }) => {
|
|
85
|
+
try {
|
|
86
|
+
const output = execSync(command, { encoding: 'utf-8', timeout: 30000 });
|
|
87
|
+
return output || 'Comando executado sem retorno visual.';
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return `Erro ao executar comando: ${err.stderr || err.message}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export async function handleToolCalls(toolCalls) {
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const call of toolCalls) {
|
|
98
|
+
const tool = tools.find(t => t.name === call.name);
|
|
99
|
+
if (tool) {
|
|
100
|
+
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
101
|
+
const result = await tool.execute(call.args);
|
|
102
|
+
results.push({
|
|
103
|
+
callId: call.id,
|
|
104
|
+
name: call.name,
|
|
105
|
+
result
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return results;
|
|
110
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
const config = new Conf({ projectName: 'bimmo-cli' });
|
|
6
|
+
|
|
7
|
+
const providers = {
|
|
8
|
+
openai: { baseURL: 'https://api.openai.com/v1', defaultModel: 'gpt-4o' },
|
|
9
|
+
anthropic: { baseURL: 'https://api.anthropic.com/v1', defaultModel: 'claude-3-5-sonnet-20240620' },
|
|
10
|
+
grok: { baseURL: 'https://api.x.ai/v1', defaultModel: 'grok-2-1212' },
|
|
11
|
+
gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta', defaultModel: 'gemini-2.0-flash' },
|
|
12
|
+
ollama: { baseURL: 'http://localhost:11434/api', defaultModel: 'llama3.2' },
|
|
13
|
+
openrouter: { baseURL: 'https://openrouter.ai/api/v1', defaultModel: 'google/gemini-2.0-flash-exp:free' },
|
|
14
|
+
deepseek: { baseURL: 'https://api.deepseek.com', defaultModel: 'deepseek-chat' },
|
|
15
|
+
zai: { baseURL: 'https://api.z.ai/v1', defaultModel: 'glm-4' }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function configure() {
|
|
19
|
+
console.log(chalk.cyan('🔧 Configuração do bimmo-cli\n'));
|
|
20
|
+
|
|
21
|
+
const profiles = config.get('profiles') || {};
|
|
22
|
+
const profileList = Object.keys(profiles);
|
|
23
|
+
|
|
24
|
+
const { action } = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: 'list',
|
|
27
|
+
name: 'action',
|
|
28
|
+
message: 'O que deseja fazer?',
|
|
29
|
+
choices: [
|
|
30
|
+
{ name: 'Criar novo perfil de IA', value: 'create' },
|
|
31
|
+
{ name: 'Selecionar perfil ativo', value: 'select' },
|
|
32
|
+
{ name: 'Configurar chave Tavily', value: 'tavily' },
|
|
33
|
+
{ name: 'Sair', value: 'exit' }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
if (action === 'exit') return;
|
|
39
|
+
|
|
40
|
+
if (action === 'tavily') {
|
|
41
|
+
const { tavilyKey } = await inquirer.prompt([{
|
|
42
|
+
type: 'input',
|
|
43
|
+
name: 'tavilyKey',
|
|
44
|
+
message: 'Chave de API Tavily:',
|
|
45
|
+
default: config.get('tavilyKey')
|
|
46
|
+
}]);
|
|
47
|
+
config.set('tavilyKey', tavilyKey);
|
|
48
|
+
console.log(chalk.green('✓ Chave Tavily salva.'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (action === 'select') {
|
|
53
|
+
if (profileList.length === 0) {
|
|
54
|
+
console.log(chalk.yellow('Nenhum perfil encontrado. Crie um primeiro.'));
|
|
55
|
+
return configure();
|
|
56
|
+
}
|
|
57
|
+
const { selected } = await inquirer.prompt([{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'selected',
|
|
60
|
+
message: 'Escolha o perfil para ativar:',
|
|
61
|
+
choices: profileList
|
|
62
|
+
}]);
|
|
63
|
+
const p = profiles[selected];
|
|
64
|
+
config.set('provider', p.provider);
|
|
65
|
+
config.set('apiKey', p.apiKey);
|
|
66
|
+
config.set('model', p.model);
|
|
67
|
+
config.set('baseURL', p.baseURL);
|
|
68
|
+
config.set('activeProfile', selected);
|
|
69
|
+
console.log(chalk.green(`✓ Perfil "${selected}" ativado!`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const answers = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: 'input',
|
|
76
|
+
name: 'profileName',
|
|
77
|
+
message: 'Dê um nome para este perfil (ex: MeuGPT, ClaudeTrabalho):',
|
|
78
|
+
validate: i => i.length > 0 || 'Nome obrigatório'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'list',
|
|
82
|
+
name: 'provider',
|
|
83
|
+
message: 'Qual provedor?',
|
|
84
|
+
choices: Object.keys(providers)
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'apiKey',
|
|
89
|
+
message: 'API Key:',
|
|
90
|
+
validate: i => i.length > 5 || 'Chave inválida'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'input',
|
|
94
|
+
name: 'model',
|
|
95
|
+
message: 'Modelo padrão (vazio para default):'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'input',
|
|
99
|
+
name: 'customBaseURL',
|
|
100
|
+
message: 'URL customizada (vazio para default):'
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const newProfile = {
|
|
105
|
+
provider: answers.provider,
|
|
106
|
+
apiKey: answers.apiKey,
|
|
107
|
+
model: answers.model || providers[answers.provider].defaultModel,
|
|
108
|
+
baseURL: answers.customBaseURL || providers[answers.provider].baseURL
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
profiles[answers.profileName] = newProfile;
|
|
112
|
+
config.set('profiles', profiles);
|
|
113
|
+
|
|
114
|
+
// Define como ativo imediatamente
|
|
115
|
+
config.set('provider', newProfile.provider);
|
|
116
|
+
config.set('apiKey', newProfile.apiKey);
|
|
117
|
+
config.set('model', newProfile.model);
|
|
118
|
+
config.set('baseURL', newProfile.baseURL);
|
|
119
|
+
config.set('activeProfile', answers.profileName);
|
|
120
|
+
|
|
121
|
+
console.log(chalk.green(`\n✅ Perfil "${answers.profileName}" criado e ativado!`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getConfig() {
|
|
125
|
+
return config.store;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function updateActiveModel(newModel) {
|
|
129
|
+
config.set('model', newModel);
|
|
130
|
+
// Também atualiza no perfil se houver um ativo
|
|
131
|
+
const active = config.get('activeProfile');
|
|
132
|
+
if (active) {
|
|
133
|
+
const profiles = config.get('profiles');
|
|
134
|
+
profiles[active].model = newModel;
|
|
135
|
+
config.set('profiles', profiles);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function switchProfile(name) {
|
|
140
|
+
const profiles = config.get('profiles') || {};
|
|
141
|
+
if (profiles[name]) {
|
|
142
|
+
const p = profiles[name];
|
|
143
|
+
config.set('provider', p.provider);
|
|
144
|
+
config.set('apiKey', p.apiKey);
|
|
145
|
+
config.set('model', p.model);
|
|
146
|
+
config.set('baseURL', p.baseURL);
|
|
147
|
+
config.set('activeProfile', name);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
package/src/interface.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import figlet from 'figlet';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
import TerminalRenderer from 'marked-terminal';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import mime from 'mime-types';
|
|
10
|
+
|
|
11
|
+
import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
|
|
12
|
+
import { createProvider } from './providers/factory.js';
|
|
13
|
+
import { getProjectContext } from './project-context.js';
|
|
14
|
+
|
|
15
|
+
marked.use(new TerminalRenderer({
|
|
16
|
+
heading: chalk.hex('#c084fc').bold,
|
|
17
|
+
code: chalk.hex('#00ff9d'),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const green = chalk.hex('#00ff9d');
|
|
21
|
+
const lavender = chalk.hex('#c084fc');
|
|
22
|
+
const gray = chalk.gray;
|
|
23
|
+
const bold = chalk.bold;
|
|
24
|
+
const yellow = chalk.yellow;
|
|
25
|
+
|
|
26
|
+
let currentMode = 'chat'; // 'chat', 'plan', 'edit'
|
|
27
|
+
|
|
28
|
+
async function processInput(input) {
|
|
29
|
+
const parts = input.split(' ');
|
|
30
|
+
const processedContent = [];
|
|
31
|
+
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
if (part.startsWith('@')) {
|
|
34
|
+
const filePath = part.slice(1);
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(filePath)) {
|
|
37
|
+
const stats = fs.statSync(filePath);
|
|
38
|
+
if (stats.isFile()) {
|
|
39
|
+
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
|
40
|
+
|
|
41
|
+
if (mimeType.startsWith('image/')) {
|
|
42
|
+
const base64Image = fs.readFileSync(filePath, { encoding: 'base64' });
|
|
43
|
+
processedContent.push({
|
|
44
|
+
type: 'image',
|
|
45
|
+
mimeType,
|
|
46
|
+
data: base64Image,
|
|
47
|
+
fileName: path.basename(filePath)
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
const textContent = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
processedContent.push({
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
processedContent.push({ type: 'text', text: part });
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
processedContent.push({ type: 'text', text: part });
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
processedContent.push({ type: 'text', text: part });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const hasImage = processedContent.some(c => c.type === 'image');
|
|
69
|
+
|
|
70
|
+
if (!hasImage) {
|
|
71
|
+
return processedContent.map(c => c.text).join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const finalContent = [];
|
|
75
|
+
let currentText = "";
|
|
76
|
+
|
|
77
|
+
for (const item of processedContent) {
|
|
78
|
+
if (item.type === 'text') {
|
|
79
|
+
currentText += (currentText ? " " : "") + item.text;
|
|
80
|
+
} else {
|
|
81
|
+
if (currentText) {
|
|
82
|
+
finalContent.push({ type: 'text', text: currentText });
|
|
83
|
+
currentText = "";
|
|
84
|
+
}
|
|
85
|
+
finalContent.push(item);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (currentText) finalContent.push({ type: 'text', text: currentText });
|
|
89
|
+
|
|
90
|
+
return finalContent;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getModeStyle() {
|
|
94
|
+
switch (currentMode) {
|
|
95
|
+
case 'plan': return yellow.bold(' [PLAN] ');
|
|
96
|
+
case 'edit': return chalk.red.bold(' [EDIT] ');
|
|
97
|
+
default: return lavender.bold(' [CHAT] ');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function startInteractive() {
|
|
102
|
+
let config = getConfig();
|
|
103
|
+
|
|
104
|
+
if (!config.provider || !config.apiKey) {
|
|
105
|
+
console.log(lavender(figlet.textSync('bimmo', { font: 'slant' })));
|
|
106
|
+
console.log(gray('\nBem-vindo! Vamos configurar seus perfis de IA.\n'));
|
|
107
|
+
await configure();
|
|
108
|
+
return startInteractive();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let provider = createProvider(config);
|
|
112
|
+
const messages = [];
|
|
113
|
+
|
|
114
|
+
// 1. Injeta o sistema de contexto inteligente do projeto
|
|
115
|
+
const projectContext = getProjectContext();
|
|
116
|
+
messages.push({
|
|
117
|
+
role: 'system',
|
|
118
|
+
content: projectContext
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.clear();
|
|
122
|
+
console.log(lavender(figlet.textSync('bimmo', { font: 'small' })));
|
|
123
|
+
console.log(lavender('─'.repeat(60)));
|
|
124
|
+
console.log(green(` Perfil Ativo: ${bold(config.activeProfile || 'Padrão')} (${config.provider.toUpperCase()})`));
|
|
125
|
+
console.log(green(` Modelo: ${bold(config.model)}`));
|
|
126
|
+
console.log(gray(' /chat | /plan | /edit | /switch [perfil] | /model [novo] | /help'));
|
|
127
|
+
console.log(lavender('─'.repeat(60)) + '\n');
|
|
128
|
+
|
|
129
|
+
console.log(lavender('👋 Olá! Sou seu agente BIMMO. No que posso atuar?\n'));
|
|
130
|
+
|
|
131
|
+
while (true) {
|
|
132
|
+
const modeIndicator = getModeStyle();
|
|
133
|
+
const { input } = await inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: 'input',
|
|
136
|
+
name: 'input',
|
|
137
|
+
message: modeIndicator + green('Você'),
|
|
138
|
+
prefix: '→',
|
|
139
|
+
}
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const rawInput = input.trim();
|
|
143
|
+
const cmd = rawInput.toLowerCase();
|
|
144
|
+
|
|
145
|
+
if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') {
|
|
146
|
+
console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
|
|
151
|
+
if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
|
|
152
|
+
if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red('⚠️ Modo EDIT.\n')); continue; }
|
|
153
|
+
|
|
154
|
+
// /switch [perfil] -> Troca Perfil + Chave + Provedor + Modelo instantaneamente
|
|
155
|
+
if (cmd.startsWith('/switch ')) {
|
|
156
|
+
const profileName = rawInput.split(' ')[1];
|
|
157
|
+
if (profileName && switchProfile(profileName)) {
|
|
158
|
+
config = getConfig(); // Atualiza config local
|
|
159
|
+
provider = createProvider(config); // Recria provedor com nova chave/url
|
|
160
|
+
console.log(green(`\n✓ Trocado para o perfil "${bold(profileName)}"!`));
|
|
161
|
+
console.log(gray(` IA: ${config.provider.toUpperCase()} | Modelo: ${config.model}\n`));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.red(`\n✖ Perfil "${profileName}" não encontrado.\n`));
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// /model [modelo] -> Troca apenas o modelo do Perfil atual
|
|
169
|
+
if (cmd.startsWith('/model ')) {
|
|
170
|
+
const newModel = rawInput.split(' ')[1];
|
|
171
|
+
if (newModel) {
|
|
172
|
+
updateActiveModel(newModel);
|
|
173
|
+
config.model = newModel;
|
|
174
|
+
provider = createProvider(config);
|
|
175
|
+
console.log(green(`\n✓ Modelo atualizado para: ${bold(newModel)}\n`));
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (cmd === '/clear') {
|
|
181
|
+
messages.length = 0;
|
|
182
|
+
messages.push({ role: 'system', content: getProjectContext() });
|
|
183
|
+
console.clear();
|
|
184
|
+
console.log(lavender('✓ Histórico limpo, contexto preservado.\n'));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (cmd === '/help') {
|
|
189
|
+
console.log(gray(`
|
|
190
|
+
Comandos Disponíveis:
|
|
191
|
+
/chat /plan /edit → Mudar modo de operação
|
|
192
|
+
/switch [nome] → Mudar PERFIL (Troca Chave/API/IA completa)
|
|
193
|
+
/model [nome] → Mudar apenas o MODELO da IA atual
|
|
194
|
+
/init → Inicializar .bimmorc.json neste projeto
|
|
195
|
+
/config → Gerenciar perfis e chaves
|
|
196
|
+
/clear → Resetar conversa (mantém contexto base)
|
|
197
|
+
@caminho → Anexar arquivos ou imagens
|
|
198
|
+
`));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (cmd === '/init') {
|
|
203
|
+
const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
|
|
204
|
+
if (fs.existsSync(bimmoRcPath)) {
|
|
205
|
+
const { overwrite } = await inquirer.prompt([{
|
|
206
|
+
type: 'confirm',
|
|
207
|
+
name: 'overwrite',
|
|
208
|
+
message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
|
|
209
|
+
default: false
|
|
210
|
+
}]);
|
|
211
|
+
if (!overwrite) continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const initialConfig = {
|
|
215
|
+
projectName: path.basename(process.cwd()),
|
|
216
|
+
rules: [
|
|
217
|
+
"Siga as convenções de código existentes.",
|
|
218
|
+
"Prefira código limpo e modular.",
|
|
219
|
+
"Sempre valide mudanças antes de aplicar no modo EDIT."
|
|
220
|
+
],
|
|
221
|
+
preferredTech: [],
|
|
222
|
+
architecture: "Não especificada",
|
|
223
|
+
ignorePatterns: ["node_modules", "dist", ".git"]
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
|
|
227
|
+
console.log(green(`\n✅ Arquivo .bimmorc.json criado com sucesso em: ${bold(bimmoRcPath)}\n`));
|
|
228
|
+
|
|
229
|
+
// Recarrega o contexto para a conversa atual
|
|
230
|
+
messages.push({
|
|
231
|
+
role: 'system',
|
|
232
|
+
content: `Novo contexto inicializado via /init:\n${JSON.stringify(initialConfig, null, 2)}`
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
|
|
238
|
+
|
|
239
|
+
if (rawInput === '') continue;
|
|
240
|
+
|
|
241
|
+
// Injeção dinâmica de instruções de modo
|
|
242
|
+
let modeInstr = "";
|
|
243
|
+
if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Descreva e analise, mas NÃO altere arquivos.";
|
|
244
|
+
else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Você tem permissão para usar write_file e run_command AGORA.";
|
|
245
|
+
|
|
246
|
+
const content = await processInput(rawInput);
|
|
247
|
+
messages.push({
|
|
248
|
+
role: 'user',
|
|
249
|
+
content: typeof content === 'string' ? content + modeInstr : [...content, { type: 'text', text: modeInstr }]
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const spinner = ora({
|
|
253
|
+
text: lavender(`bimmo (${currentMode}) pensando...`),
|
|
254
|
+
color: currentMode === 'edit' ? 'red' : 'magenta'
|
|
255
|
+
}).start();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const responseText = await provider.sendMessage(messages);
|
|
259
|
+
spinner.stop();
|
|
260
|
+
messages.push({ role: 'assistant', content: responseText });
|
|
261
|
+
|
|
262
|
+
console.log('\n' + lavender('bimmo') + getModeStyle());
|
|
263
|
+
console.log(lavender('─'.repeat(50)));
|
|
264
|
+
console.log(marked(responseText));
|
|
265
|
+
console.log(gray('─'.repeat(50)) + '\n');
|
|
266
|
+
} catch (err) {
|
|
267
|
+
spinner.stop();
|
|
268
|
+
console.error(chalk.red('✖ Erro Crítico:') + ' ' + err.message + '\n');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Coleta o contexto completo do projeto para a IA.
|
|
7
|
+
* Inclui estrutura de arquivos, arquivos de instrução e configurações do .bimmorc
|
|
8
|
+
*/
|
|
9
|
+
export function getProjectContext() {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
let context = "=== CONTEXTO DO PROJETO ===\n";
|
|
12
|
+
|
|
13
|
+
// 1. Tentar ler .bimmorc.json para regras customizadas
|
|
14
|
+
const bimmoRcPath = path.join(cwd, '.bimmorc.json');
|
|
15
|
+
if (fs.existsSync(bimmoRcPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const rc = JSON.parse(fs.readFileSync(bimmoRcPath, 'utf-8'));
|
|
18
|
+
context += `Regras de Projeto (.bimmorc):\n${JSON.stringify(rc, null, 2)}\n\n`;
|
|
19
|
+
} catch (e) {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Tentar ler arquivos de instruções comuns (Claude, Gemini, etc)
|
|
23
|
+
const instructionFiles = ['CLAUDE.md', 'INSTRUCTIONS.md', '.bimmo-context.md', 'CONTRIBUTING.md'];
|
|
24
|
+
for (const file of instructionFiles) {
|
|
25
|
+
const p = path.join(cwd, file);
|
|
26
|
+
if (fs.existsSync(p)) {
|
|
27
|
+
context += `Instruções de ${file}:\n${fs.readFileSync(p, 'utf-8')}\n\n`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Adicionar estrutura de diretórios (Quantizada/Resumida)
|
|
32
|
+
try {
|
|
33
|
+
const tree = execSync('find . -maxdepth 2 -not -path "*/.*" -not -path "./node_modules*"', { encoding: 'utf-8' });
|
|
34
|
+
context += `Estrutura de Arquivos (Resumo):\n${tree}\n`;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
context += "Estrutura de arquivos indisponível.\n";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { BaseProvider } from './base.js';
|
|
3
|
+
import { tools } from '../agent.js';
|
|
4
|
+
|
|
5
|
+
export class AnthropicProvider extends BaseProvider {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super(config);
|
|
8
|
+
this.client = new Anthropic({
|
|
9
|
+
apiKey: this.config.apiKey
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
formatContent(content) {
|
|
14
|
+
if (typeof content === 'string') return content;
|
|
15
|
+
return content.map(part => {
|
|
16
|
+
if (part.type === 'text') return { type: 'text', text: part.text };
|
|
17
|
+
if (part.type === 'image') return {
|
|
18
|
+
type: 'image',
|
|
19
|
+
source: { type: 'base64', media_type: part.mimeType, data: part.data }
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async sendMessage(messages) {
|
|
25
|
+
const systemMessage = messages.find(m => m.role === 'system');
|
|
26
|
+
const userMessages = messages
|
|
27
|
+
.filter(m => m.role !== 'system')
|
|
28
|
+
.map(m => ({
|
|
29
|
+
role: m.role,
|
|
30
|
+
content: this.formatContent(m.content)
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Converte tools do agent.js para o formato da Anthropic
|
|
34
|
+
const anthropicTools = tools.map(t => ({
|
|
35
|
+
name: t.name,
|
|
36
|
+
description: t.description,
|
|
37
|
+
input_schema: t.parameters
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const response = await this.client.messages.create({
|
|
41
|
+
model: this.config.model,
|
|
42
|
+
max_tokens: 4096,
|
|
43
|
+
system: systemMessage ? systemMessage.content : undefined,
|
|
44
|
+
messages: userMessages,
|
|
45
|
+
tools: anthropicTools,
|
|
46
|
+
temperature: 0.7
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (response.stop_reason === 'tool_use') {
|
|
50
|
+
const toolUse = response.content.find(p => p.type === 'tool_use');
|
|
51
|
+
const tool = tools.find(t => t.name === toolUse.name);
|
|
52
|
+
|
|
53
|
+
if (tool) {
|
|
54
|
+
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
55
|
+
const result = await tool.execute(toolUse.input);
|
|
56
|
+
|
|
57
|
+
// Adiciona a resposta da IA e o resultado da tool ao histórico
|
|
58
|
+
const nextMessages = [
|
|
59
|
+
...messages,
|
|
60
|
+
{ role: 'assistant', content: response.content },
|
|
61
|
+
{
|
|
62
|
+
role: 'user',
|
|
63
|
+
content: [{
|
|
64
|
+
type: 'tool_result',
|
|
65
|
+
tool_use_id: toolUse.id,
|
|
66
|
+
content: String(result)
|
|
67
|
+
}]
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return this.sendMessage(nextMessages);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return response.content[0].text;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OpenAIProvider } from './openai.js';
|
|
2
|
+
import { AnthropicProvider } from './anthropic.js';
|
|
3
|
+
import { GrokProvider } from './grok.js';
|
|
4
|
+
import { GeminiProvider } from './gemini.js';
|
|
5
|
+
import { OllamaProvider } from './ollama.js';
|
|
6
|
+
|
|
7
|
+
export function createProvider(config) {
|
|
8
|
+
switch (config.provider) {
|
|
9
|
+
case 'openai': return new OpenAIProvider(config);
|
|
10
|
+
case 'anthropic': return new AnthropicProvider(config);
|
|
11
|
+
case 'grok': return new GrokProvider(config);
|
|
12
|
+
case 'gemini': return new GeminiProvider(config);
|
|
13
|
+
case 'ollama': return new OllamaProvider(config);
|
|
14
|
+
case 'openrouter': return new OpenAIProvider(config);
|
|
15
|
+
case 'deepseek': return new OpenAIProvider(config);
|
|
16
|
+
case 'zai': return new OpenAIProvider(config);
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`Provider ${config.provider} não suportado ainda.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { BaseProvider } from './base.js';
|
|
3
|
+
import { tools } from '../agent.js';
|
|
4
|
+
|
|
5
|
+
export class GeminiProvider extends BaseProvider {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super(config);
|
|
8
|
+
this.genAI = new GoogleGenerativeAI(this.config.apiKey);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
formatContent(content) {
|
|
12
|
+
if (typeof content === 'string') return [{ text: content }];
|
|
13
|
+
return content.map(part => {
|
|
14
|
+
if (part.type === 'text') return { text: part.text };
|
|
15
|
+
if (part.type === 'image') return {
|
|
16
|
+
inlineData: { mimeType: part.mimeType, data: part.data }
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async sendMessage(messages) {
|
|
22
|
+
const systemPrompt = messages.find(m => m.role === 'system')?.content;
|
|
23
|
+
const history = messages
|
|
24
|
+
.filter(m => m.role !== 'system')
|
|
25
|
+
.slice(0, -1)
|
|
26
|
+
.map(msg => ({
|
|
27
|
+
role: msg.role === 'user' ? 'user' : 'model',
|
|
28
|
+
parts: this.formatContent(msg.content)
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const lastMessageContent = this.formatContent(messages[messages.length - 1].content);
|
|
32
|
+
|
|
33
|
+
// Converte tools do agent.js para o formato do Gemini
|
|
34
|
+
const geminiTools = tools.map(t => ({
|
|
35
|
+
functionDeclarations: [{
|
|
36
|
+
name: t.name,
|
|
37
|
+
description: t.description,
|
|
38
|
+
parameters: t.parameters
|
|
39
|
+
}]
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const model = this.genAI.getGenerativeModel({
|
|
43
|
+
model: this.config.model,
|
|
44
|
+
systemInstruction: systemPrompt,
|
|
45
|
+
tools: geminiTools
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const chat = model.startChat({
|
|
49
|
+
history: history,
|
|
50
|
+
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = await chat.sendMessage(lastMessageContent);
|
|
54
|
+
const response = await result.response;
|
|
55
|
+
|
|
56
|
+
// Processamento de Tool Calls no Gemini
|
|
57
|
+
const call = response.candidates[0].content.parts.find(p => p.functionCall);
|
|
58
|
+
if (call) {
|
|
59
|
+
const tool = tools.find(t => t.name === call.functionCall.name);
|
|
60
|
+
if (tool) {
|
|
61
|
+
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
62
|
+
const result = await tool.execute(call.functionCall.args);
|
|
63
|
+
|
|
64
|
+
// No Gemini, enviamos o resultado de volta para o chat
|
|
65
|
+
const resultResponse = await chat.sendMessage([{
|
|
66
|
+
functionResponse: {
|
|
67
|
+
name: call.functionCall.name,
|
|
68
|
+
response: { content: result }
|
|
69
|
+
}
|
|
70
|
+
}]);
|
|
71
|
+
return resultResponse.response.text();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return response.text();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { BaseProvider } from './base.js';
|
|
3
|
+
|
|
4
|
+
export class GrokProvider extends BaseProvider {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super(config);
|
|
7
|
+
this.client = new OpenAI({
|
|
8
|
+
apiKey: this.config.apiKey,
|
|
9
|
+
baseURL: this.config.baseURL || 'https://api.x.ai/v1'
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
formatMessages(messages) {
|
|
14
|
+
return messages.map(msg => {
|
|
15
|
+
if (typeof msg.content === 'string') {
|
|
16
|
+
return msg;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const content = msg.content.map(part => {
|
|
20
|
+
if (part.type === 'text') {
|
|
21
|
+
return { type: 'text', text: part.text };
|
|
22
|
+
} else if (part.type === 'image') {
|
|
23
|
+
return {
|
|
24
|
+
type: 'image_url',
|
|
25
|
+
image_url: {
|
|
26
|
+
url: `data:${part.mimeType};base64,${part.data}`
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return { ...msg, content };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async sendMessage(messages) {
|
|
37
|
+
const formattedMessages = this.formatMessages(messages);
|
|
38
|
+
|
|
39
|
+
const response = await this.client.chat.completions.create({
|
|
40
|
+
model: this.config.model,
|
|
41
|
+
messages: formattedMessages,
|
|
42
|
+
temperature: 0.7
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return response.choices[0].message.content;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ollama from 'ollama';
|
|
2
|
+
import { BaseProvider } from './base.js';
|
|
3
|
+
|
|
4
|
+
export class OllamaProvider extends BaseProvider {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super(config);
|
|
7
|
+
this.client = ollama;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
formatMessages(messages) {
|
|
11
|
+
return messages.map(msg => {
|
|
12
|
+
if (typeof msg.content === 'string') return msg;
|
|
13
|
+
|
|
14
|
+
const images = msg.content
|
|
15
|
+
.filter(part => part.type === 'image')
|
|
16
|
+
.map(part => part.data); // Ollama espera apenas a string base64
|
|
17
|
+
|
|
18
|
+
const text = msg.content
|
|
19
|
+
.filter(part => part.type === 'text')
|
|
20
|
+
.map(part => part.text)
|
|
21
|
+
.join(' ');
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
role: msg.role,
|
|
25
|
+
content: text,
|
|
26
|
+
images: images.length > 0 ? images : undefined
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async sendMessage(messages) {
|
|
32
|
+
const formattedMessages = this.formatMessages(messages);
|
|
33
|
+
|
|
34
|
+
const response = await this.client.chat({
|
|
35
|
+
model: this.config.model,
|
|
36
|
+
messages: formattedMessages,
|
|
37
|
+
stream: false,
|
|
38
|
+
options: {
|
|
39
|
+
temperature: 0.7,
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const text = response.message?.content;
|
|
44
|
+
|
|
45
|
+
if (!text) {
|
|
46
|
+
throw new Error('Resposta inválida do Ollama');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { BaseProvider } from './base.js';
|
|
3
|
+
import { tools } from '../agent.js';
|
|
4
|
+
|
|
5
|
+
export class OpenAIProvider extends BaseProvider {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super(config);
|
|
8
|
+
this.client = new OpenAI({
|
|
9
|
+
apiKey: this.config.apiKey,
|
|
10
|
+
baseURL: this.config.baseURL
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
formatMessages(messages) {
|
|
15
|
+
return messages.map(msg => {
|
|
16
|
+
if (typeof msg.content === 'string') return msg;
|
|
17
|
+
|
|
18
|
+
const content = msg.content.map(part => {
|
|
19
|
+
if (part.type === 'text') return { type: 'text', text: part.text };
|
|
20
|
+
if (part.type === 'image') return {
|
|
21
|
+
type: 'image_url',
|
|
22
|
+
image_url: { url: `data:${part.mimeType};base64,${part.data}` }
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
return { ...msg, content };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async sendMessage(messages) {
|
|
30
|
+
const formattedMessages = this.formatMessages(messages);
|
|
31
|
+
|
|
32
|
+
// Converte tools do agent.js para o formato da OpenAI
|
|
33
|
+
const openAiTools = tools.map(t => ({
|
|
34
|
+
type: 'function',
|
|
35
|
+
function: {
|
|
36
|
+
name: t.name,
|
|
37
|
+
description: t.description,
|
|
38
|
+
parameters: t.parameters
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const response = await this.client.chat.completions.create({
|
|
43
|
+
model: this.config.model,
|
|
44
|
+
messages: formattedMessages,
|
|
45
|
+
tools: openAiTools,
|
|
46
|
+
tool_choice: 'auto'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const message = response.choices[0].message;
|
|
50
|
+
|
|
51
|
+
if (message.tool_calls) {
|
|
52
|
+
const toolResults = [];
|
|
53
|
+
for (const toolCall of message.tool_calls) {
|
|
54
|
+
const tool = tools.find(t => t.name === toolCall.function.name);
|
|
55
|
+
if (tool) {
|
|
56
|
+
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
57
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
58
|
+
const result = await tool.execute(args);
|
|
59
|
+
|
|
60
|
+
toolResults.push({
|
|
61
|
+
role: 'tool',
|
|
62
|
+
tool_call_id: toolCall.id,
|
|
63
|
+
content: String(result)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Adiciona a chamada da tool e o resultado ao histórico
|
|
69
|
+
const nextMessages = [...formattedMessages, message, ...toolResults];
|
|
70
|
+
|
|
71
|
+
// Chamada recursiva para processar a resposta final da IA com o resultado da tool
|
|
72
|
+
return this.sendMessage(nextMessages);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return message.content;
|
|
76
|
+
}
|
|
77
|
+
}
|