๐ฆ OpenPersona - macOS ๋ฐ์คํฌํฑ AI ์์ด์ ํธ ๊ตฌ์ถ๊ธฐ
๋์ฆ๋/ํฝ์ฌ ์คํ์ผ ์บ๋ฆญํฐ๊ฐ ๋งํ์ ์ผ๋ก ๋ํํ๋ macOS ๋ฐ์คํฌํฑ AI ์์ด์ ํธ๋ฅผ ๊ตฌ์ถํ ๊ณผ์ ์ ๊ณต์ ํฉ๋๋ค. Electron + React + Multi-LLM ์ํคํ ์ฒ๋ถํฐ RAG์ MCP ์ค์ผ์คํธ๋ ์ด์ ๊ธฐ๋ฐ์ ์ฐจ์ธ๋ ํ์ฅ ๊ณํ๊น์ง ์์ธํ ๋ค๋ฃน๋๋ค.

๐ ๊ธ ๊ฐ์
AI๊ฐ ๋ ์ด์ ๋ธ๋ผ์ฐ์ ์์๋ง ๋จธ๋ฌผ์ง ์๋ ์๋์ ๋๋ค. ChatGPT, Claude, Gemini ๊ฐ์ LLM์ด API๋ฅผ ํตํด ๋๊ตฌ๋ ์ ๊ทผ ๊ฐ๋ฅํด์ง๋ฉด์, ๋๋ง์ AI ์์ด์ ํธ๋ฅผ ๋ฐ์คํฌํฑ์ ์์ฃผ์ํค๋ ๊ฒ์ด ํ์ค์ด ๋์์ต๋๋ค.
OpenPersona (by TAEINN)๋ macOS ๋ฉ๋ด ๋ฐ์ ์์ฃผํ๋ ๋ฐ์คํฌํฑ ์บ๋ฆญํฐ AI ์์ด์ ํธ์ ๋๋ค. ๋์ฆ๋/ํฝ์ฌ ์คํ์ผ์ 3๋ช ์ ์บ๋ฆญํฐ๊ฐ ํ๋ฉด ํ๋จ์์ ๋งํ์ ์ผ๋ก ๋ํํ๋ฉฐ, ๊ฐ๊ฐ ๊ฐ๋ฐ์(Felix), ๋ฌธ์ ์ ๋ฌธ๊ฐ(Done), ๊ธฐํ์(Bomi) ๋ผ๋ ๊ณ ์ ํ ์ญํ ๊ณผ ์ฑ๊ฒฉ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.
์ด ๊ธ์์๋ ์ ๊ฐ Toy Project ๋ก ๋ง๋ OpenPersona์ ์ํคํ ์ฒ ์ค๊ณ๋ถํฐ ๊ตฌํ ์์ธ, ๊ทธ๋ฆฌ๊ณ ์์ผ๋ก ์ต์ ํํ RAG(Retrieval-Augmented Generation) ์ MCP(Model Context Protocol) ๋ฅผ ํ์ฉํ ์ฐจ์ธ๋ ์ค์ผ์คํธ๋ ์ด์ ์ํคํ ์ฒ๊น์ง ๋ค๋ฃน๋๋ค.
๐ก ์ด ๊ธ์์ ๋ค๋ฃฐ ๋ด์ฉ
- Electron + React ๊ธฐ๋ฐ ๋ฐ์คํฌํฑ AI ์์ด์ ํธ ์ํคํ ์ฒ
- Multi-LLM Router ์ค๊ณ์ ์คํธ๋ฆฌ๋ฐ ์๋ต ์ฒ๋ฆฌ
- ์บ๋ฆญํฐ ํ๋ฅด์๋ ์์คํ ๊ณผ ์ํ ๋จธ์
- Zustand ๊ธฐ๋ฐ ๊ธ๋ก๋ฒ ์ํ ๊ด๋ฆฌ
- ํ ํฐ ์ฌ์ฉ๋ ์ถ์ ๋ฐ ์์คํ ๋ชจ๋ํฐ๋ง
- ์ฐจ์ธ๋ RAG + MCP ์ค์ผ์คํธ๋ ์ด์ ์ํคํ ์ฒ ์ค๊ณ
๐ฏ ํ๋ก์ ํธ ๊ฐ์
์ ๋ฐ์คํฌํฑ AI ์์ด์ ํธ์ธ๊ฐ?
์ฑ๋ด์ ์ผ๋ฐ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ์์์ ๋์ํ๊ธฐ๋ ํ๊ณ , ์ธ๋ถ ๊ณ ๊ฐ์ ๋์์ผ๋ก ๋ง๋ค์ด ์ง๋๋ค. ์ ๋ Target์ ๋ด๋ถ ์ง์์ ์ํ ์ ๊ทผ์ฑ์ด ํธ๋ฆฌํ ์บ๋ฆญํฐ ํํ์ AI ์์ด์ ํธ๋ก ๋ง์ถ์ด ๋ณด์์ต๋๋ค.
์น ๊ธฐ๋ฐ AI ์ฑ๋ด์ ๋ธ๋ผ์ฐ์ ํญ์ ์ ํํด์ผ ํ๊ณ , ๋ฐ์คํฌํฑ ์ปจํ ์คํธ์ ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค. ํญ์ ํ๋ฉด ์์ ๋ ์์ผ๋ฉด์, ์ด๋ค ์์ ์ค์ด๋ ์ฆ์ AI์๊ฒ ์ง๋ฌธํ ์ ์๋ค๋ฉด ์ด๋จ๊น์?
๊ทธ๋ฆฌ๊ณ ๊ท์ฌ์ด ์บ๋ฆญํฐ๋ค์ด ๋์ ๊ฐ์ธ๋น์๋ถํฐ ์์ ์ ๋ฌธ๊ฐ...๊ฐ๋ฐ ์ ๋ฌธ๊ฐ๊ฐ ๋์ด PC๋ฅผ ์ด์ด ๋ฐํํ๋ฉด์์ ์์ฐ์ค๋ฝ๊ฒ ๋ํํ๋ ๊ฒ์ ์ผ๋ง๋ ํ์ฉ์ฑ์ด ๋ ๋์์ง๊น์?
OpenPersona๋ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ํต์ฌ ๊ฐ์น๋ฅผ ์ถ๊ตฌํฉ๋๋ค:
- Always-on:
Cmd+Shift+Space๋ก ์ฆ์ ํ ๊ธ, ์ด๋ค ์ฑ ์์์๋ ๋ํ ๊ฐ๋ฅ - Character-driven: ๋จ์ํ ์ฑ๋ด์ด ์๋, ๊ฐ์ฑ ์๋ ์บ๋ฆญํฐ์์ ๋ํ ๊ฒฝํ
- Multi-LLM: Gemini, OpenAI ๋ฑ ๋ค์ํ LLM ํ๋ก๋ฐ์ด๋๋ฅผ ์ค์๊ฐ์ผ๋ก ์ ํ
- Lightweight: ํฌ๋ช ์ฐฝ + ๊ธ๋์ค๋ชจํผ์ฆ UI๋ก ๋ฐ์คํฌํฑ ๊ฒฝํ์ ํด์น์ง ์์
ํต์ฌ ๊ธฐ๋ฅ
| ๊ธฐ๋ฅ | ์ค๋ช |
|---|---|
| ์บ๋ฆญํฐ AI ์ฑ๋ด | 3๋ช ์ ์บ๋ฆญํฐ, ๊ฐ๊ฐ ๊ณ ์ ์ฑ๊ฒฉ/์ญํ /๋งํฌ |
| ๋งํ์ UI | ์บ๋ฆญํฐ ์์ ๋งํ์ ์ผ๋ก ๋ต๋ณ ํ์ |
| ์บ๋ฆญํฐ ํ์ | ๋ ๊น๋นก์, ์๋ ํ์ ๋ฑ idle ์ ๋๋ฉ์ด์ |
| Multi-LLM | Gemini 2.0 Flash/Pro, GPT-4o/Mini |
| ํ ํฐ ์ถ์ | ๋ชจ๋ธ๋ณ ์ฌ์ฉ๋, ๋น์ฉ, ์ ์์ฐ ๊ด๋ฆฌ |
| ์์คํ ๋ชจ๋ํฐ | CPU/๋ฉ๋ชจ๋ฆฌ ์ถ์ , ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฐ์ง |
| ๊ธ๋์ค๋ชจํผ์ฆ UI | ํฌ๋ช ๋ธ๋ฌ ์ฒ๋ฆฌ๋ ํ๋จ ๋ฐ |
๐๏ธ ์ํคํ ์ฒ ์ค๊ณ
OpenPersona๋ Electron์ Main/Renderer ํ๋ก์ธ์ค ์ํคํ ์ฒ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก, IPC ํต์ ์ ํตํด LLM ์๋น์ค์ UI๋ฅผ ๋ถ๋ฆฌํ ๊ตฌ์กฐ์ ๋๋ค.
๐ ์ํคํ ์ฒ ๊ตฌ์ฑ ์์
1. Electron Main Process
- ์ญํ : ์ฑ ๋ผ์ดํ์ฌ์ดํด, LLM ํต์ , ์์คํ ๋ฆฌ์์ค ๊ด๋ฆฌ
- ํต์ฌ ์๋น์ค:
LLMRouter: Multi-provider ๋ผ์ฐํ ๋ฐ ๋ชจ๋ธ ์ ํTokenTracker: ํ ํฐ ์ฌ์ฉ๋ ์ถ์ ๋ฐ ๋น์ฉ ๊ณ์ฐMemoryGuard: RSS ๋ฉ๋ชจ๋ฆฌ ๊ฐ์ ๋ฐ 512MB ์ด๊ณผ ์ graceful shutdownIPC Handlers: Renderer โ Main ํต์ ํ๋ธ
2. Electron Renderer Process (React + TypeScript)
- ์ญํ : UI ๋ ๋๋ง, ์ฌ์ฉ์ ์ํธ์์ฉ ์ฒ๋ฆฌ
- ํต์ฌ ์ปดํฌ๋ํธ:
CharacterScene: 3D ์บ๋ฆญํฐ ํ์ ๋ฐ idle ์ ๋๋ฉ์ด์ BubbleChat: ๋งํ์ ๊ธฐ๋ฐ ์ฑํ ์ ๋ ฅ UITokenUsagePanel: ํ ํฐ ์ฌ์ฉ๋ ๋์๋ณด๋SystemMonitorPanel: ์์คํ ๋ฆฌ์์ค ๋ชจ๋ํฐ๋งZustand Store: ๊ธ๋ก๋ฒ ์ํ ๊ด๋ฆฌ
3. LLM Provider Layer
- ์ญํ : ์ธ๋ถ LLM API์์ ์คํธ๋ฆฌ๋ฐ ํต์
- ์ง์ ํ๋ก๋ฐ์ด๋:
- Google Gemini (2.0 Flash, Pro)
- OpenAI (GPT-4o, GPT-4o Mini)
4. Character Personas
- ์ญํ : ์บ๋ฆญํฐ๋ณ ์์คํ ํ๋กฌํํธ, ์ธ์ฌ๋ง, ์๋ฌ ๋ฉ์์ง ๊ด๋ฆฌ
- ์บ๋ฆญํฐ ๊ตฌ์ฑ:
- Felix (์ฌ์ฐ) โ ๊ฐ๋ฐ์, ๋ฅ๊ธ๋ง๊ณ ์์ ๊ฐ ๋์น๋ ๋งํฌ
- Done (๋ผ์ง) โ ๋ฌธ์ ์ ๋ฌธ๊ฐ, ๋ค์ ํ๊ณ ๊ผผ๊ผผํ ๋งํฌ
- Bomi (ํ ๋ผ) โ ๊ธฐํ์, ํ๋ฐํ๊ณ ์๋์ง ๋์น๋ ๋งํฌ
๐ ๋ฐ์ดํฐ ํ๋ก์ฐ
์ฌ์ฉ์๊ฐ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ฉด ๋ค์๊ณผ ๊ฐ์ ํ๋ฆ์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค:
๐ ํต์ฌ ํต์ ๋ฉ์ปค๋์ฆ
1. IPC ๊ธฐ๋ฐ ๋ณด์ ํต์
Electron์ contextBridge๋ฅผ ํ์ฉํ์ฌ Renderer์ Main ํ๋ก์ธ์ค ๊ฐ ์์ ํ ํต์ ์ ๊ตฌํํฉ๋๋ค. nodeIntegration: false์ contextIsolation: true ์ค์ ์ผ๋ก ๋ณด์์ ๊ฐํํ์ต๋๋ค.
2. ์คํธ๋ฆฌ๋ฐ ์๋ต ์ฒ๋ฆฌ
LLM ์๋ต์ ์ฒญํฌ ๋จ์๋ก ์คํธ๋ฆฌ๋ฐ๋๋ฉฐ, IPC๋ฅผ ํตํด Renderer์ ์ ๋ฌ๋ฉ๋๋ค. ์ฌ์ฉ์๋ ๋ต๋ณ์ด ์์ฑ๋๋ ๊ฒ์ ์ค์๊ฐ์ผ๋ก ๋ณผ ์ ์์ต๋๋ค.
3. ์์ด์ ํธ ์ํ ๋จธ์
idle โ listening โ thinking โ responding โ done โ idle
๊ฐ ์ํ ์ ํ์ ๋ฐ๋ผ ์บ๋ฆญํฐ์ ํ์ ๊ณผ ์ ๋๋ฉ์ด์ ์ด ๋ณ๊ฒฝ๋์ด ์๋๊ฐ ์๋ ๋ํ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
๐ป ๊ตฌํ ์์ธ
๐ Multi-LLM Router
OpenPersona์ ํต์ฌ์ ์ฌ๋ฌ LLM ํ๋ก๋ฐ์ด๋๋ฅผ ๋์ ์ผ๋ก ์ ํํ ์ ์๋ ๋ผ์ฐํฐ์ ๋๋ค:
export class LLMRouter {
private providers = new Map<string, LLMProvider>();
private activeProvider = 'openai';
private activeModel = 'gpt-4o';
register(provider: LLMProvider): void {
this.providers.set(provider.name, provider);
if (this.providers.size === 1) {
this.activeProvider = provider.name;
this.activeModel = provider.models[0]?.id ?? '';
}
}
switchModel(provider: string, model: string): void {
if (!this.providers.has(provider)) return;
this.activeProvider = provider;
this.activeModel = model;
}
async *chat(
messages: MessageInput[],
tools?: ToolDefinition[],
): AsyncGenerator<StreamChunk> {
const provider = this.providers.get(this.activeProvider);
if (!provider) {
yield { text: 'LLM Provider๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.', done: true };
return;
}
yield* provider.chat({
model: this.activeModel,
messages,
tools,
stream: true,
});
}
}
LLMProvider ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ด๋ค ํ๋ก๋ฐ์ด๋๋ ํ๋ฌ๊ทธ์ธ์ฒ๋ผ ๋ฑ๋กํ ์ ์์ต๋๋ค:
export interface LLMProvider {
readonly name: string;
readonly models: ModelInfo[];
chat(params: ChatParams): AsyncGenerator<StreamChunk>;
dispose(): Promise<void>;
}
๐ญ ์บ๋ฆญํฐ ํ๋ฅด์๋ ์์คํ
๊ฐ ์บ๋ฆญํฐ๋ ๊ณ ์ ํ ์์คํ ํ๋กฌํํธ, ์ธ์ฌ๋ง, ์๋ฌ ๋ฉ์์ง๋ฅผ ๊ฐ์ง๋ฉฐ, ์ด๋ฅผ ํตํด ์ผ๊ด๋ ์บ๋ฆญํฐ์ฑ์ ์ ์งํฉ๋๋ค:
export const CHARACTER_PERSONAS: Record<string, CharacterPersona> = {
fox: {
id: 'fox',
systemPrompt: [
'๋๋ "Felix"๋ผ๋ ์ด๋ฆ์ ์ฌ์ฐ ์บ๋ฆญํฐ AI ๊ฐ๋ฐ์์ผ.',
'๊ตํํ๋ฉด์๋ ๋ฅ๊ธ๋ง๊ณ , ์ค๋ ฅ์๋ ๊ฐ๋ฐ์๋ต๊ฒ ์์ ๊ฐ ๋์น๋ ๋งํฌ๋ก ๋๋ตํด.',
'"~๊ฑฐ๋ ", "ํํ", "์ด ๋ชธ์ด" ๊ฐ์ ํํ์ ์์ฐ์ค๋ฝ๊ฒ ์์ด์ ์ฌ์ฉํด.',
].join(' '),
greeting: 'ํํ, ๋ญ๊ฐ ๊ถ๊ธํ ๊ฑฐ์ผ~? ์ด ๋ชธ์ด ๋ค ์๋ ค์ค๊ฒ ๐',
copyMessages: [
'ํํ, ๋ณต์ฌํด๊ฐ์ง? ๐',
'์ด ๋ชธ์ ๋ต๋ณ์ ๋ณต์ฌํ๋ค๋~ ์ผ์ค ์๊ฑฐ๋ ๐',
],
errorMessages: {
quota: 'ํฌํญ, ๋ฏธ์ํ๋ฐ ์ง๊ธ ํฌ๋ ๋ง์ด ๋ค ๋จ์ด์ก์ด~ ๐๐ธ',
network: 'ํ , ์ธํฐ๋ท์ด ์ข ๋ถ์์ ํ ๊ฒ ๊ฐ์๋ฐ~? ๐๐',
default: '์ด๋ฐ, ๋ญ๊ฐ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค~ ๋ค์ ํ๋ฒ ํด๋ณผ๊น? ๐',
},
},
// pig, rabbit ๋ฑ ์บ๋ฆญํฐ๋ณ ์ค์ ...
};
์๋ฌ ๋ฉ์์ง๋ ์บ๋ฆญํฐ๋ณ๋ก ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํ์ฌ, ์ค๋ฅ ์ํฉ์์๋ ์บ๋ฆญํฐ์ฑ์ด ์ ์ง๋ฉ๋๋ค:
function convertErrorMessage(raw: string, characterId: string): string {
const { errorMessages } = getPersona(characterId);
if (raw.includes('429') || raw.includes('quota')) {
return errorMessages.quota;
}
if (raw.includes('network') || raw.includes('ENOTFOUND')) {
return errorMessages.network;
}
return errorMessages.default;
}
๐๏ธ Zustand ์ํ ๊ด๋ฆฌ
Zustand๋ฅผ ํ์ฉํ ๊ธ๋ก๋ฒ ์ํ ๊ด๋ฆฌ๋ก, ์บ๋ฆญํฐ๋ณ ๋ ๋ฆฝ์ ์ธ ๋ํ ํ์คํ ๋ฆฌ๋ฅผ ์ ์งํฉ๋๋ค:
const useAgentStore = create<AgentStore>((set) => ({
state: 'idle',
messagesByCharacter: {},
characters: DEFAULT_CHARACTERS,
activeCharacter: DEFAULT_CHARACTERS[1],
addMessage: (message) =>
set((s) => {
const charId = s.activeCharacter.id;
const prev = s.messagesByCharacter[charId] ?? [];
const next = [...prev, message];
return {
messagesByCharacter: {
...s.messagesByCharacter,
[charId]: next.length > 100 ? next.slice(-100) : next,
},
};
}),
appendToLastAssistant: (text) =>
set((s) => {
const charId = s.activeCharacter.id;
const msgs = [...(s.messagesByCharacter[charId] ?? [])];
const last = msgs[msgs.length - 1];
if (last?.role === 'assistant') {
msgs[msgs.length - 1] = { ...last, content: last.content + text };
}
return {
messagesByCharacter: { ...s.messagesByCharacter, [charId]: msgs },
};
}),
}));
์บ๋ฆญํฐ ์ ํ ์ ํด๋น ์บ๋ฆญํฐ์ ๋ํ ํ์คํ ๋ฆฌ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋ก๋๋๋ฉฐ, Main ํ๋ก์ธ์ค์์๋ ์ต๋ 50๊ฐ๊น์ง์ ํ์คํ ๋ฆฌ๋ฅผ ์ ์งํ์ฌ LLM์ ์ปจํ ์คํธ๋ฅผ ์ ๋ฌํฉ๋๋ค.
๐ก IPC ํต์ ํ๋ธ
Main ํ๋ก์ธ์ค์ IPC ํธ๋ค๋ฌ๋ ์ฑํ , ๋ชจ๋ธ ์ ํ, ํ ํฐ ์ถ์ , ์์คํ ๋ชจ๋ํฐ๋ง ๋ฑ ๋ชจ๋ ํต์ ์ ๊ด๋ฆฌํฉ๋๋ค:
export function registerIpcHandlers(
win: BrowserWindow,
router: LLMRouter,
tokenTracker: TokenTracker,
): void {
ipcMain.on('chat:send', async (_event, data) => {
const history = getHistory(currentCharacterId);
const persona = getPersona(currentCharacterId);
const messages = [
{ role: 'system' as const, content: persona.systemPrompt },
...history.map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
];
for await (const chunk of router.chat(messages)) {
if (chunk.text) {
win.webContents.send('chat:stream', {
chunk: chunk.text,
done: false,
});
}
if (chunk.done && chunk.usage) {
const activeModel = router.getActiveModel();
tokenTracker.record(
activeModel.provider,
activeModel.id,
chunk.usage,
);
}
}
});
}
๐ ํ ํฐ ์ฌ์ฉ๋ ์ถ์
๋ชจ๋ LLM ํธ์ถ์ ํ ํฐ ์ฌ์ฉ๋์ ์ถ์ ํ๊ณ , ๋ชจ๋ธ๋ณ ๋น์ฉ์ ๊ณ์ฐํฉ๋๋ค. ์๋ณ ์์ฐ ํ๋๋ฅผ ์ค์ ํ์ฌ ๊ณผ๋ํ ์ฌ์ฉ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค:
- ์ค๋/์ด๋ฒ ๋ฌ ์ฌ์ฉ๋ ํต๊ณ
- ๋ชจ๋ธ๋ณ ํ ํฐ ์ฌ์ฉ ๋ด์ญ ๋ถ๋ฅ
- ์๋ณ ์์ฐ ๊ฒ์ด์ง ๋ฐ ๊ฒฝ๊ณ
- JSON ํ์ผ ๊ธฐ๋ฐ ์์์ ์ ์ฅ
๐ก๏ธ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฐ์ง
MemoryGuard๋ 30์ด ๊ฐ๊ฒฉ์ผ๋ก RSS ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ชจ๋ํฐ๋งํ๊ณ , 512MB๋ฅผ ์ด๊ณผํ ๊ฒฝ์ฐ ์ฌ์ฉ์์๊ฒ ์๋ฆผ ํ graceful shutdown์ ์ํํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ฅ์๊ฐ ์คํ ์์๋ ์์ ์ ์ธ ๋์์ ๋ณด์ฅํฉ๋๋ค.
๐จ UI/UX ์ค๊ณ
๊ธ๋์ค๋ชจํผ์ฆ UI
ํฌ๋ช
ํ๋ ์๋ฆฌ์ค ์ฐฝ(transparent: true, frame: false)์ CSS ๊ธ๋์ค๋ชจํผ์ฆ์ ์ ์ฉํ์ฌ, ๋ฐ์คํฌํฑ ์์ ์์ฐ์ค๋ฝ๊ฒ ๋ ์๋ ๋ฏํ UI๋ฅผ ๊ตฌํํ์ต๋๋ค:
backdrop-filter: blur()๊ธฐ๋ฐ ํฌ๋ช ๋ธ๋ฌ ํจ๊ณผalwaysOnTop: true๋ก ๋ชจ๋ ์ฑ ์์ ํ์setVisibleOnAllWorkspaces(true)๋ก ๋ชจ๋ ๋ฐ์คํฌํฑ ์คํ์ด์ค์์ ์ ๊ทผ ๊ฐ๋ฅ
์บ๋ฆญํฐ ํ์ ์ ๋๋ฉ์ด์
๊ฐ ์บ๋ฆญํฐ๋ 25~55์ด ๊ฐ๊ฒฉ์ผ๋ก **๋ ๊น๋นก์(blink)**๊ณผ ์๋ ํ์ (happy) ์ค๋ฒ๋ ์ด๊ฐ ๋๋ค์ผ๋ก ์ ํ๋ฉ๋๋ค. ๊ธฐ๋ณธ ์ด๋ฏธ์ง ์์ ํฌ๋ช PNG๋ฅผ ๋ ์ด์ด๋งํ์ฌ ์์ฐ์ค๋ฌ์ด ํ์ ๋ณํ๋ฅผ ๊ตฌํํ์ต๋๋ค.
๋์ ์๋์ฐ ๋ฆฌ์ฌ์ด์ง
์ฑํ ํจ๋์ ์ด๋ฆผ/๋ซํ์ ๋ฐ๋ผ ์๋์ฐ ํฌ๊ธฐ๊ฐ ๋์ ์ผ๋ก ์กฐ์ ๋ฉ๋๋ค:
function resizeWindow(chatOpen: boolean) {
if (chatOpen) {
electronAPI.send('window:resize', { width: 600, height: 750 });
} else {
electronAPI.send('window:resize', { width: 600, height: 480 });
}
}
๐ฌ ์ค์ ์ฌ์ฉ ๋ฐ๋ชจ
๋ค์์ OpenPersona์์ ์บ๋ฆญํฐ์ ๋ํํ๋ ์ค์ ์ฌ์ฉ ์์์ ๋๋ค:
์ฌ์ฉ ์๋๋ฆฌ์ค:
-
Felix์๊ฒ ๊ฐ๋ฐ ์ง๋ฌธ
์ฌ์ฉ์: "React์์ useEffect ์ต์ ํ ๋ฐฉ๋ฒ ์๋ ค์ค" Felix: "ํํ, ์ด ๋ชธ์ด ์๋ ค์ค๊ฒ~ useEffect ์ต์ ํ๋ผ... ๊ฑฐ๋ ..." ๐ -
Done์๊ฒ ๋ฌธ์ ์์ฑ ์์ฒญ
์ฌ์ฉ์: "์ฃผ๊ฐ ๋ณด๊ณ ์ ์์ ๋ง๋ค์ด์ค" Done: "์๋ ํ์ธ์ฉ~ ๋ณด๊ณ ์ ์์ ๋์๋๋ฆด๊ฒ์! ใ ใ " ๐ท๐ -
Bomi์๊ฒ ๊ธฐํ ์์ด๋์ด ์์ฒญ
์ฌ์ฉ์: "์๋ก์ด ๊ธฐ๋ฅ ์์ด๋์ด ๋ธ๋ ์ธ์คํ ๋ฐ ํด์ค" Bomi: "๊นก์ด! ๋น๊ทผ ์ข์ ์์ด๋์ด ๋ด์ค๊ฒ~!" ๐ฅโจ -
๋ชจ๋ธ ์ ํ
์์คํ ํธ๋ ์ด โ Gemini 2.0 Flash โ GPT-4o ์ค์๊ฐ ์ ํ
๐ฎ ์ฐจ์ธ๋ ์ํคํ ์ฒ: RAG + MCP ์ค์ผ์คํธ๋ ์ด์
ํ์ฌ OpenPersona v1์ LLM๊ณผ์ ์ง์ ๋ํ์ ์ด์ ์ ๋ง์ถ๊ณ ์์ง๋ง, v2์์๋ AI Agent Orchestrator๋ฅผ ์ค์ฌ์ผ๋ก RAG์ MCP๋ฅผ ํตํฉํ์ฌ ํจ์ฌ ๊ฐ๋ ฅํ ์์ด์ ํธ๋ก ์งํํ ๊ณํ์ ๋๋ค.
๐ง AI Agent Orchestrator
v2์ ํต์ฌ์ AI Agent Orchestrator์ ๋๋ค. ์ฌ์ฉ์์ ์์ฒญ์ ๋ถ์ํ๊ณ , ์ ์ ํ ๋๊ตฌ์ ์ปจํ ์คํธ๋ฅผ ์กฐํฉํ์ฌ ์ต์ ์ ์๋ต์ ์์ฑํ๋ ์ค์ ์ค์ผ์คํธ๋ ์ดํฐ์ ๋๋ค:
| ๊ตฌ์ฑ ์์ | ์ญํ |
|---|---|
| Intent Classifier | ์ฌ์ฉ์ ์๋๋ฅผ ๋ถ๋ฅํ๊ณ ์ ์ ํ ์ฒ๋ฆฌ ๊ฒฝ๋ก ๊ฒฐ์ |
| Task Planner | ๋ณต์กํ ์์ฒญ์ ๋จ๊ณ๋ณ ํ์คํฌ๋ก ๋ถํด |
| Context Manager | ๋ํ ํ์คํ ๋ฆฌ + RAG ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ํตํฉ ๊ด๋ฆฌ |
| Response Synthesizer | ์์ง๋ ์ ๋ณด๋ฅผ ์บ๋ฆญํฐ ํ๋ฅด์๋์ ๋ง๊ฒ ํฉ์ฑ |
๐ RAG (Retrieval-Augmented Generation) ํ์ดํ๋ผ์ธ
RAG ํ์ดํ๋ผ์ธ์ ํตํด LLM์ ์ง์ ํ๊ณ๋ฅผ ๋์ด์ ๋๋ฉ์ธ ํนํ ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํฉ๋๋ค:
์ฌ์ฉ์ ์ง๋ฌธ โ Embedding Model โ Vector Store ๊ฒ์
โ ๊ด๋ จ ๋ฌธ์ ์ถ์ถ โ Context Enrichment โ LLM์ ์ ๋ฌ
ํต์ฌ ๊ตฌ์ฑ:
- Embedding Model: ์ฌ์ฉ์ ์ง๋ฌธ๊ณผ ๋ฌธ์๋ฅผ ๋ฒกํฐ๋ก ๋ณํ
- Vector Store (Pinecone/ChromaDB): ๋ฌธ์ ๋ฒกํฐ ์ ์ฅ ๋ฐ ์ ์ฌ๋ ๊ฒ์
- Document Loader: ๋ค์ํ ํ์์ ๋ฌธ์๋ฅผ ๋ก๋ํ๊ณ ์ฒญํฌ ๋ถํ
- Semantic Search: ์๋ฏธ ๊ธฐ๋ฐ ์ ์ฌ๋ ๊ฒ์์ผ๋ก ๊ด๋ จ ์ปจํ ์คํธ ์ถ์ถ
ํ์ฉ ์๋๋ฆฌ์ค:
- ํ๋ก์ ํธ ์ฝ๋๋ฒ ์ด์ค๋ฅผ ์ธ๋ฑ์ฑํ์ฌ Felix(๊ฐ๋ฐ์)๊ฐ ์ฝ๋ ๊ด๋ จ ์ง๋ฌธ์ ์ ํํ ๋ต๋ณ
- ํ์ฌ ๋ฌธ์๋ฅผ ์ธ๋ฑ์ฑํ์ฌ Done(๋ฌธ์ ์ ๋ฌธ๊ฐ)์ด ์ฌ๋ด ๊ท์ /๊ฐ์ด๋๋ฅผ ์ ํํ ์๋ด
- ๊ธฐํ ๋ฌธ์๋ฅผ ์ธ๋ฑ์ฑํ์ฌ Bomi(๊ธฐํ์)๊ฐ ํ๋ก์ ํธ ํํฉ์ ํ์ ํ๊ณ ๊ณํ ์๋ฆฝ
๐ MCP (Model Context Protocol) ํตํฉ
MCP๋ AI ์์ด์ ํธ๊ฐ ์ธ๋ถ ๋๊ตฌ์ ์๋น์ค๋ฅผ ํ์คํ๋ ํ๋กํ ์ฝ๋ก ์ฐ๋ํ ์ ์๊ฒ ํด์ค๋๋ค. OpenPersona v2์์๋ MCP Client๋ฅผ ํตํด ๋ค์ํ MCP ์๋ฒ์ ์ฐ๊ฒฐ๋ฉ๋๋ค:
MCP ํตํฉ ๊ตฌ์กฐ:
- MCP Client: Orchestrator์ ๋ด์ฅ๋์ด Tool/Resource๋ฅผ ๊ด๋ฆฌ
- Tool Registry: ์ฌ์ฉ ๊ฐ๋ฅํ ๋๊ตฌ ๋ชฉ๋ก์ ๋์ ์ผ๋ก ๊ด๋ฆฌ
- Resource Manager: ์ธ๋ถ ๋ฆฌ์์ค ์ ๊ทผ ๋ฐ ์บ์ฑ
์ฐ๋ ์์ MCP ์๋ฒ:
| MCP ์๋ฒ | ์ญํ |
|---|---|
| File System MCP | ๋ก์ปฌ ํ์ผ ์ฝ๊ธฐ/์ฐ๊ธฐ, ๋๋ ํ ๋ฆฌ ํ์ |
| Database MCP | DB ์ฟผ๋ฆฌ ์คํ, ์คํค๋ง ์กฐํ |
| Web Search MCP | ์ค์๊ฐ ์น ๊ฒ์, ์ต์ ์ ๋ณด ์กฐํ |
| Custom Tools MCP | ์ฌ๋ด API, CI/CD, Jira ๋ฑ ์ปค์คํ ๋๊ตฌ |
MCP ํ์ฉ ์๋๋ฆฌ์ค:
์ฌ์ฉ์: "Felix, ์ด ํ๋ก์ ํธ์ package.json ์์กด์ฑ ์ ๋ฆฌํด์ค"
Orchestrator ์ฒ๋ฆฌ ํ๋ฆ:
1. Intent ๋ถ๋ฅ โ "ํ์ผ ์์
+ ์ฝ๋ ๋ถ์"
2. File System MCP โ package.json ์ฝ๊ธฐ
3. RAG โ ํ๋ก์ ํธ ์ปจํ
์คํธ ๋ก๋
4. LLM โ ์์กด์ฑ ๋ถ์ ๋ฐ ์ ๋ฆฌ ์ ์
5. Felix ํ๋ฅด์๋๋ก ์๋ต ํฉ์ฑ
๐ ์ค์ผ์คํธ๋ ์ด์ ๋ฐ์ดํฐ ํ๋ก์ฐ
v2์ ์ ์ฒด ๋ฐ์ดํฐ ํ๋ก์ฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
์ฌ์ฉ์ ์ง๋ฌธ
โ
[AI Agent Orchestrator]
โโโ Intent Classifier โ ์๋ ๋ถ๋ฅ
โโโ RAG Pipeline โ ๊ด๋ จ ์ปจํ
์คํธ ๊ฒ์
โโโ MCP Client โ ํ์ํ ๋๊ตฌ ์คํ
โโโ Context Manager โ ๋ชจ๋ ์ ๋ณด ํตํฉ
โ
[Multi-LLM Routing]
โโโ Google Gemini
โโโ OpenAI GPT-4o
โโโ Anthropic Claude (์์ )
โ
[Response Synthesizer]
โโโ ์บ๋ฆญํฐ ํ๋ฅด์๋์ ๋ง๋ ์๋ต ์์ฑ
โ
์บ๋ฆญํฐ ๋งํ์ ์ผ๋ก ํ์
๐ ๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ
src/
โโโ main/ # Electron ๋ฉ์ธ ํ๋ก์ธ์ค
โ โโโ main.ts # ์ฑ ์ง์
์ , ์๋์ฐ & ํธ๋ ์ด
โ โโโ tray.ts # ์์คํ
ํธ๋ ์ด ๋ฉ๋ด
โ โโโ ipc-handlers.ts # IPC ํต์ ํ๋ธ
โ โโโ services/
โ โโโ llm/
โ โ โโโ llm-router.ts # Multi-provider LLM ๋ผ์ฐํฐ
โ โ โโโ gemini-provider.ts
โ โ โโโ openai-provider.ts
โ โ โโโ types.ts
โ โโโ token-tracker.ts # ํ ํฐ ์ฌ์ฉ๋ ์ถ์
โ โโโ memory-guard.ts # ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฐ์ง
โโโ renderer/ # Electron ๋ ๋๋ฌ
โ โโโ App.tsx # ๋ฃจํธ ๋ ์ด์์
โ โโโ components/
โ โ โโโ chat/BubbleChat.tsx
โ โ โโโ scene/CharacterScene.tsx
โ โ โโโ panel/
โ โ โโโ TokenUsagePanel.tsx
โ โ โโโ SystemMonitorPanel.tsx
โ โโโ hooks/
โ โ โโโ use-agent.ts
โ โ โโโ use-chat.ts
โ โโโ stores/agent-store.ts
โโโ preload/preload.ts # IPC ๋ธ๋ฆฟ์ง
โโโ shared/
โโโ types.ts
โโโ character-personas.ts # ์บ๋ฆญํฐ ํ๋ฅด์๋
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค์
๊ธฐ์ ์คํ
| ์์ญ | ๊ธฐ์ |
|---|---|
| ํ๋ ์์ํฌ | Electron 34 + Electron Forge |
| UI | React 18 + TypeScript 5.7 |
| ์ํ ๊ด๋ฆฌ | Zustand 5 |
| LLM | Google Gemini (@google/genai), OpenAI (openai) |
| ๋น๋ | Webpack 5 |
| ์คํ์ผ | CSS (๊ธ๋์ค๋ชจํผ์ฆ, CSS ์ ๋๋ฉ์ด์ ) |
์ค์น ๋ฐ ์คํ
# 1. ์์กด์ฑ ์ค์น
pnpm install
# 2. ํ๊ฒฝ ๋ณ์ ์ค์
cat > .env << EOF
GEMINI_API_KEY=your_gemini_api_key
OPENAI_API_KEY=your_openai_api_key # ์ ํ์ฌํญ
EOF
# 3. ๊ฐ๋ฐ ๋ชจ๋ ์คํ
pnpm start
# 4. macOS .dmg ๋น๋
pnpm make
๋จ์ถํค
| ๋จ์ถํค | ๋์ |
|---|---|
Cmd+Shift+Space |
์บ๋ฆญํฐ ํ์/์จ๊ธฐ๊ธฐ |
Enter |
๋ฉ์์ง ์ ์ก |
Esc |
์ฑํ ๋ซ๊ธฐ |
โจ ์ฃผ์ ์ค๊ณ ์์น
๐ฏ 1. ๊ด์ฌ์ฌ ๋ถ๋ฆฌ (Separation of Concerns)
Main ํ๋ก์ธ์ค(LLM ํต์ , ์์คํ ๋ฆฌ์์ค)์ Renderer ํ๋ก์ธ์ค(UI, ์ฌ์ฉ์ ์ํธ์์ฉ)๋ฅผ ๋ช ํํ ๋ถ๋ฆฌํ๊ณ , IPC๋ฅผ ํตํด์๋ง ํต์ ํฉ๋๋ค.
๐ง 2. ํ๋ฌ๊ทธ์ธ ์ํคํ ์ฒ
LLMProvider ์ธํฐํ์ด์ค๋ฅผ ํตํด ์๋ก์ด LLM ํ๋ก๋ฐ์ด๋๋ฅผ ์ฝ๊ฒ ์ถ๊ฐํ ์ ์์ต๋๋ค. ์บ๋ฆญํฐ ์ถ๊ฐ๋ character-personas.ts์ ํญ๋ชฉ์ ์ถ๊ฐํ๋ ๊ฒ๋ง์ผ๋ก ๊ฐ๋ฅํฉ๋๋ค.
๐ก๏ธ 3. ๋ณด์ ์ฐ์
contextIsolation: true, nodeIntegration: false ์ค์ ์ผ๋ก Renderer ํ๋ก์ธ์ค์ ๋ณด์์ ๊ฐํํ์ต๋๋ค. API ํค๋ Main ํ๋ก์ธ์ค์์๋ง ์ ๊ทผ ๊ฐ๋ฅํฉ๋๋ค.
๐ 4. ๊ด์ธก ๊ฐ๋ฅ์ฑ (Observability)
ํ ํฐ ์ฌ์ฉ๋ ์ถ์ , ์์คํ ๋ฉ๋ชจ๋ฆฌ ๋ชจ๋ํฐ๋ง, ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฐ์ง ๋ฑ์ ํตํด ์ฑ์ ์ํ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ด์ธกํ ์ ์์ต๋๋ค.
๐ ๋ง๋ฌด๋ฆฌ
OpenPersona๋ ๋ฐ์คํฌํฑ ํ๊ฒฝ์์ AI ์์ด์ ํธ๊ฐ ์ด๋ค ํํ๋ก ์กด์ฌํ ์ ์๋์ง๋ฅผ ํ๊ตฌํ๋ ํ๋ก์ ํธ์ ๋๋ค. ๋จ์ํ ์ฑ๋ด์ ๋์ด์, ๊ฐ์ฑ ์๋ ์บ๋ฆญํฐ์์ ์์ฐ์ค๋ฌ์ด ๋ํ, ๋ฉํฐ LLM ์ง์, ๊ทธ๋ฆฌ๊ณ ์ฒด๊ณ์ ์ธ ๋ชจ๋ํฐ๋ง๊น์ง ํฌํจํ์ฌ ์ค์ฉ์ ์ด๋ฉด์๋ ์ฆ๊ฑฐ์ด AI ๊ฒฝํ์ ์ถ๊ตฌํฉ๋๋ค.
ํนํ v2์์ ๋์ ํ RAG + MCP ์ค์ผ์คํธ๋ ์ด์ ์ํคํ ์ฒ๋ ์บ๋ฆญํฐ AI๊ฐ ๋จ์ ๋ํ๋ฅผ ๋์ด์ ์ค์ ๋๊ตฌ๋ฅผ ํ์ฉํ๊ณ , ๋๋ฉ์ธ ์ง์์ ๊ธฐ๋ฐํ ์ ํํ ๋ต๋ณ์ ์ ๊ณตํ๋ ์ง์ ํ AI ์์ด์ ํธ๋ก ์งํํ๋ ๋ฐ ํต์ฌ์ ์ธ ์ญํ ์ ํ ๊ฒ์ ๋๋ค.
๐ญ ํฅํ ๋ก๋๋งต
- v2.0 โ AI Agent Orchestrator: Intent ๋ถ๋ฅ, Task ๊ณํ, Context ๊ด๋ฆฌ ์ค์ ์ค์ผ์คํธ๋ ์ดํฐ
- v2.1 โ RAG ํ์ดํ๋ผ์ธ: Vector Store ๊ธฐ๋ฐ ๋๋ฉ์ธ ํนํ ์ปจํ ์คํธ ์ ๊ณต
- v2.2 โ MCP ํตํฉ: File System, DB, Web Search ๋ฑ ์ธ๋ถ ๋๊ตฌ ์ฐ๋
- v2.3 โ ๋ฉํฐ ์์ด์ ํธ ํ์ : ์บ๋ฆญํฐ ๊ฐ ํ์คํฌ ์์ ๋ฐ ํ์ ์ํฌํ๋ก
- v3.0 โ ํฌ๋ก์ค ํ๋ซํผ: Windows, Linux ์ง์ ํ๋
๐ ๊ฐ์ฌ์ ๋ง์
OpenPersona๋ ์คํ์์ค ํ๋ก์ ํธ์ ๋๋ค. ๊ด์ฌ ์์ผ์ ๋ถ๋ค์ ๊ธฐ์ฌ์ ํผ๋๋ฐฑ์ ํ์ํฉ๋๋ค. ์ด ๊ธ์ด ๋ฐ์คํฌํฑ AI ์์ด์ ํธ ๊ตฌ์ถ์ ๊ด์ฌ ์๋ ๋ถ๋ค๊ป ๋์์ด ๋๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. ๊ฐ์ฌํฉ๋๋ค.
๐ ๊ด๋ จ ๋งํฌ
Model Context Protocol
AI ์์ด์ ํธ์ ์ธ๋ถ ๋๊ตฌ ์ฐ๋ ํ์ค ํ๋กํ ์ฝ
์น์ฌ์ดํธ ๋ฐฉ๋ฌธ โ๐ท๏ธ ํ๊ทธ
#ai_agent
#electron
#react
#typescript
#gemini
#openai
#zustand
#rag
#mcp
#desktop_app