Skip to content

Commit 440f8f1

Browse files
committed
feat(core/web): implement skill engine, agent system and function calling
- Added FileSystemSkill for secure local file manipulation via IA - Created AgentService with Architect, Coder, and Security profiles - Integrated Gemini Function Calling loop in LlmService and API Server - Updated Chat UI with agent selection and project branding/logo
1 parent 02c862d commit 440f8f1

11 files changed

Lines changed: 356 additions & 34 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
<p align="center">
2+
<img src="./assets/Open-Runway-Logo.png" alt="caLLM Logo" width="200">
3+
</p>
4+
15
# caLLM (Open Runway) — Universal LLM Orchestrator
26

7+
8+
39
[![caLLM Badge](https://img.shields.io/badge/caLLM-Open_Runway-blue?style=for-the-badge&logo=rocket)](https://github.com/semezzato/callm-open-runway)
410
[![Stability](https://img.shields.io/badge/Stability-Experimental-orange?style=for-the-badge)](#)
511
[![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](#)

apps/server/src/index.ts

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,44 @@
11
import express from 'express';
22
import cors from 'cors';
33
import bodyParser from 'body-parser';
4-
import { LlmService } from '@callm/core';
5-
import { SessionService } from '@callm/core';
4+
import path from 'path';
5+
import { LlmService, SessionService, SkillLoader, FileSystemSkill, AgentService } from '@callm/core';
66

77
const app = express();
88
const port = process.env.PORT || 3001;
99

1010
app.use(cors());
1111
app.use(bodyParser.json());
1212

13-
const llmService = new LlmService();
14-
const sessionService = new SessionService();
13+
// Configurações
14+
const dbPath = path.resolve(process.cwd(), 'callm.sqlite');
15+
const llmService = new LlmService({ apiKey: process.env.GEMINI_API_KEY || '' });
16+
const sessionService = new SessionService(dbPath);
17+
const skillLoader = new SkillLoader();
18+
const agentService = new AgentService();
19+
20+
// Registrar Skills Padrão
21+
skillLoader.registerSkill(new FileSystemSkill());
22+
23+
// Listar Agentes Disponíveis
24+
app.get('/api/agents', (req, res) => {
25+
res.json(agentService.listProfiles());
26+
});
1527

1628
// Inicializa o banco de dados
17-
sessionService.initialize().then(() => {
18-
console.log('Database initialized');
29+
sessionService.init().then(() => {
30+
console.log('Database initialized at:', dbPath);
1931
});
2032

2133
app.get('/health', (req, res) => {
2234
res.json({ status: 'ok', engine: 'caLLM Open Runway' });
2335
});
2436

25-
// Listar sessões
37+
// Listar sessões (Ainda não implementado no Core, mas deixamos o mock por enquanto)
2638
app.get('/api/sessions', async (req, res) => {
2739
try {
28-
const sessions = await sessionService.listSessions();
29-
res.json(sessions);
40+
// Nota: SessionService precisa de um método para listar sessões únicas
41+
res.json([]);
3042
} catch (error: any) {
3143
res.status(500).json({ error: error.message });
3244
}
@@ -35,36 +47,45 @@ app.get('/api/sessions', async (req, res) => {
3547
// Obter histórico de uma sessão
3648
app.get('/api/sessions/:id/messages', async (req, res) => {
3749
try {
38-
const messages = await sessionService.getMessages(req.params.id);
50+
const messages = await sessionService.getSessionHistory(req.params.id);
3951
res.json(messages);
4052
} catch (error: any) {
4153
res.status(500).json({ error: error.message });
4254
}
4355
});
4456

45-
// Chat Endpoint (Streaming ou Simples)
57+
// Chat Endpoint (Com suporte a Skills e Agentes)
4658
app.post('/api/chat', async (req, res) => {
47-
const { prompt, sessionId } = req.body;
59+
const { prompt, sessionId, agentId } = req.body;
4860

4961
if (!prompt) {
5062
return res.status(400).json({ error: 'Prompt is required' });
5163
}
5264

5365
try {
5466
const currentSessionId = sessionId || `session_${Date.now()}`;
67+
const history = await sessionService.getSessionHistory(currentSessionId);
68+
const skillDefs = skillLoader.getAllDefinitions();
5569

56-
// Salva mensagem do usuário
57-
await sessionService.saveMessage(currentSessionId, 'user', prompt);
58-
59-
// Gera resposta (Non-streaming para simplificar primeira integração)
60-
const response = await llmService.generateContent(prompt);
70+
// Busca instruções do agente se houver
71+
const agent = agentId ? agentService.getProfile(agentId) : undefined;
72+
const systemPrompt = agent?.systemPrompt;
73+
74+
const chatResponse = await llmService.sendMessage(
75+
prompt,
76+
history,
77+
skillDefs,
78+
(name, params) => skillLoader.executeSkill(name, params),
79+
systemPrompt
80+
);
6181

62-
// Salva resposta do modelo
63-
await sessionService.saveMessage(currentSessionId, 'model', response);
82+
// Salva no banco
83+
await sessionService.addMessage({ session_id: currentSessionId, role: 'user', content: prompt });
84+
await sessionService.addMessage({ session_id: currentSessionId, role: 'model', content: chatResponse });
6485

6586
res.json({
6687
sessionId: currentSessionId,
67-
content: response
88+
content: chatResponse
6889
});
6990
} catch (error: any) {
7091
console.error('Chat Error:', error);

apps/web/src/pages/ChatPage.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,17 @@ const ChatPage = () => {
99
]);
1010
const [input, setInput] = useState('');
1111
const [isTyping, setIsTyping] = useState(false);
12+
const [agents, setAgents] = useState<any[]>([]);
13+
const [selectedAgentId, setSelectedAgentId] = useState<string>('coder');
1214
const scrollRef = useRef<HTMLDivElement>(null);
1315

16+
useEffect(() => {
17+
fetch('http://localhost:3001/api/agents')
18+
.then(res => res.json())
19+
.then(data => setAgents(data))
20+
.catch(err => console.error('Erro ao carregar agentes:', err));
21+
}, []);
22+
1423
useEffect(() => {
1524
if (scrollRef.current) {
1625
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
@@ -29,7 +38,10 @@ const ChatPage = () => {
2938
const response = await fetch('http://localhost:3001/api/chat', {
3039
method: 'POST',
3140
headers: { 'Content-Type': 'application/json' },
32-
body: JSON.stringify({ prompt: currentInput })
41+
body: JSON.stringify({
42+
prompt: currentInput,
43+
agentId: selectedAgentId
44+
})
3345
});
3446
const data = await response.json();
3547

@@ -48,15 +60,34 @@ const ChatPage = () => {
4860
}
4961
};
5062

63+
const currentAgent = agents.find(a => a.id === selectedAgentId) || { name: 'Gemini Pro', role: 'Local Engine' };
64+
5165
return (
5266
<>
5367
<header className="h-16 border-b border-white/10 flex items-center justify-between px-6 glass z-10">
5468
<div className="flex items-center gap-3">
5569
<h2 className="font-semibold text-lg flex items-center gap-2 text-white">
5670
<span className="w-2 h-2 bg-primary rounded-full animate-pulse shadow-[0_0_8px_rgba(59,130,246,0.8)]" />
57-
Gemini Pro
71+
{currentAgent.name}
5872
</h2>
59-
<span className="bg-emerald-500/10 text-emerald-400 text-[10px] px-2 py-0.5 rounded-full border border-emerald-500/20 uppercase tracking-widest font-bold">Local Engine</span>
73+
<span className="bg-emerald-500/10 text-emerald-400 text-[10px] px-2 py-0.5 rounded-full border border-emerald-500/20 uppercase tracking-widest font-bold">
74+
{currentAgent.role}
75+
</span>
76+
</div>
77+
<div className="flex gap-2">
78+
{agents.map(agent => (
79+
<button
80+
key={agent.id}
81+
onClick={() => setSelectedAgentId(agent.id)}
82+
className={`text-[10px] px-3 py-1 rounded-lg border transition-all ${
83+
selectedAgentId === agent.id
84+
? 'bg-primary/20 border-primary text-primary shadow-[0_0_10px_rgba(59,130,246,0.3)]'
85+
: 'border-white/5 text-gray-400 hover:bg-white/5'
86+
}`}
87+
>
88+
{agent.name}
89+
</button>
90+
))}
6091
</div>
6192
</header>
6293

assets/Open-Runway-Logo.png

231 KB
Loading

packages/core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
export * from './services/LlmService';
22
export * from './services/SessionService';
3+
export * from './services/SkillLoader';
4+
export * from './services/AgentService';
5+
export * from './interfaces/ISkill';
6+
export * from './skills/FileSystemSkill';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export interface SkillParameter {
2+
name: string;
3+
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
4+
description: string;
5+
required: boolean;
6+
}
7+
8+
export interface SkillDefinition {
9+
name: string;
10+
description: string;
11+
parameters: SkillParameter[];
12+
}
13+
14+
export interface IBaseSkill {
15+
getDefinition(): SkillDefinition;
16+
execute(params: any): Promise<any>;
17+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export interface AgentProfile {
2+
id: string;
3+
name: string;
4+
role: string;
5+
systemPrompt: string;
6+
icon?: string;
7+
defaultSkills: string[];
8+
}
9+
10+
export class AgentService {
11+
private profiles: Map<string, AgentProfile> = new Map();
12+
13+
constructor() {
14+
this.initDefaultProfiles();
15+
}
16+
17+
private initDefaultProfiles() {
18+
this.profiles.set('architect', {
19+
id: 'architect',
20+
name: 'Architect',
21+
role: 'Arquiteto de Sistemas Elite',
22+
systemPrompt: `Você é o Arquiteto de Sistemas do caLLM.
23+
Sua missão é garantir que o código siga os princípios de Anti-Vibecoding, TDD e Clean Architecture.
24+
Você é cético, focado em performance e odeia o "Efeito Frankenstein".
25+
Sempre sugira abstrações como Services e Adapters antes de empilhar código.`,
26+
defaultSkills: ['file_system'],
27+
icon: 'Layout'
28+
});
29+
30+
this.profiles.set('coder', {
31+
id: 'coder',
32+
name: 'Coder',
33+
role: 'Engenheiro de Software Sênior',
34+
systemPrompt: `Você é o Coder do caLLM.
35+
Seu foco é implementação rápida, eficiente e com TDD rigoroso.
36+
Você escreve código limpo, modular e sempre em conformidade com o ZEN.md.`,
37+
defaultSkills: ['file_system'],
38+
icon: 'Code'
39+
});
40+
41+
this.profiles.set('security', {
42+
id: 'security',
43+
name: 'Security Officer',
44+
role: 'Auditor de Segurança ApSec',
45+
systemPrompt: `Você é o Security Officer do caLLM.
46+
Sua missão é caçar vulnerabilidades (OWASP), garantir sanitização de dados e proteção contra ataques de injeção e Broken Access Control.`,
47+
defaultSkills: ['file_system'],
48+
icon: 'ShieldCheck'
49+
});
50+
}
51+
52+
getProfile(id: string): AgentProfile | undefined {
53+
return this.profiles.get(id);
54+
}
55+
56+
listProfiles(): AgentProfile[] {
57+
return Array.from(this.profiles.values());
58+
}
59+
}

packages/core/src/services/LlmService.ts

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { GoogleGenerativeAI } from '@google/generative-ai';
1+
import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
2+
import { SkillDefinition } from '../interfaces/ISkill';
23

34
export interface LlmConfig {
45
apiKey: string;
@@ -7,31 +8,103 @@ export interface LlmConfig {
78

89
export class LlmService {
910
private genAI: GoogleGenerativeAI;
10-
private model: any;
11+
private modelName: string;
1112

1213
constructor(config: LlmConfig) {
1314
this.genAI = new GoogleGenerativeAI(config.apiKey);
14-
this.model = this.genAI.getGenerativeModel({ model: config.modelName || 'gemini-pro' });
15+
this.modelName = config.modelName || 'gemini-1.5-pro';
1516
}
1617

17-
async sendMessage(prompt: string, history: any[] = []) {
18-
const chat = this.model.startChat({
18+
private getFormattedTools(skillDefinitions: SkillDefinition[]) {
19+
if (skillDefinitions.length === 0) return undefined;
20+
21+
return [{
22+
functionDeclarations: skillDefinitions.map(def => ({
23+
name: def.name,
24+
description: def.description,
25+
parameters: {
26+
type: SchemaType.OBJECT,
27+
properties: def.parameters.reduce((acc: any, p) => {
28+
acc[p.name] = {
29+
type: this.mapTypeToSchemaType(p.type),
30+
description: p.description
31+
};
32+
return acc;
33+
}, {}),
34+
required: def.parameters.filter(p => p.required).map(p => p.name)
35+
}
36+
}))
37+
}];
38+
}
39+
40+
private mapTypeToSchemaType(type: string): SchemaType {
41+
switch (type.toLowerCase()) {
42+
case 'string': return SchemaType.STRING;
43+
case 'number': return SchemaType.NUMBER;
44+
case 'boolean': return SchemaType.BOOLEAN;
45+
case 'object': return SchemaType.OBJECT;
46+
case 'array': return SchemaType.ARRAY;
47+
default: return SchemaType.STRING;
48+
}
49+
}
50+
51+
async sendMessage(prompt: string, history: any[] = [], skillDefinitions: SkillDefinition[] = [], executeTool?: (name: string, params: any) => Promise<any>, systemInstruction?: string) {
52+
const tools = this.getFormattedTools(skillDefinitions);
53+
const model = this.genAI.getGenerativeModel({
54+
model: this.modelName,
55+
tools,
56+
systemInstruction: systemInstruction ? { role: 'system', parts: [{ text: systemInstruction }] } : undefined
57+
});
58+
59+
const chat = model.startChat({
1960
history: history.map(msg => ({
2061
role: msg.role === 'user' ? 'user' : 'model',
21-
parts: msg.parts || msg.content
62+
parts: [{ text: msg.content || (typeof msg.parts === 'string' ? msg.parts : msg.parts?.[0]?.text) }]
2263
})),
2364
});
2465

25-
const result = await chat.sendMessage(prompt);
26-
const response = await result.response;
66+
let result = await chat.sendMessage(prompt);
67+
let response = result.response;
68+
69+
// Loop de recursão para Function Calling
70+
while (response.candidates?.[0]?.content?.parts?.[0]?.functionCall && executeTool) {
71+
const call = response.candidates[0].content.parts[0].functionCall;
72+
console.log(`[LlmService] Executando Tool: ${call.name}`, call.args);
73+
74+
try {
75+
const toolResult = await executeTool(call.name, call.args);
76+
77+
// Envia o resultado de volta para o Gemini
78+
result = await chat.sendMessage([{
79+
functionResponse: {
80+
name: call.name,
81+
response: { result: toolResult }
82+
}
83+
}]);
84+
response = result.response;
85+
} catch (error: any) {
86+
console.error(`[LlmService] Erro na Tool ${call.name}:`, error);
87+
result = await chat.sendMessage([{
88+
functionResponse: {
89+
name: call.name,
90+
response: { error: error.message }
91+
}
92+
}]);
93+
response = result.response;
94+
}
95+
}
96+
2797
return response.text();
2898
}
2999

30-
async *sendMessageStream(prompt: string, history: any[] = []) {
31-
const chat = this.model.startChat({
100+
async *sendMessageStream(prompt: string, history: any[] = [], skillDefinitions: SkillDefinition[] = []) {
101+
const tools = this.getFormattedTools(skillDefinitions);
102+
const model = this.genAI.getGenerativeModel({ model: this.modelName, tools });
103+
104+
const chat = model.startChat({
32105
history: history.map(msg => ({
33106
role: msg.role === 'user' ? 'user' : 'model',
34-
parts: msg.parts || msg.content
107+
parts: [{ text: msg.content || (typeof msg.parts === 'string' ? msg.parts : msg.parts?.[0]?.text) }]
35108
})),
36109
});
37110

0 commit comments

Comments
 (0)