Sitemap
TOKTOKHAN.DEV — TEAM

똑똑한개발자는 디지털 프로덕트 에이전시 입니다. 프로젝트를 진행하면서 얻게되는 기술 또는 디자인 관점에서의 인사이트를 공유하기 위해 콘텐츠를 발행하고 있습니다. We publish content to share insights gained from a technical or design perspective while working on projects.

Follow publication

ElizaOS를 활용해 자율 인공지능 에이전트 만들기:Part.3 나만의 에이전트 만들기

--

똑똑한개발자의 Autonomous AI Agent를 만드는 과정을 공유합니다!

안녕하세요. 사랑받는 IT 프로덕트의 첫걸음, 똑똑한개발자 입니다. 똑똑한개발자의 자율 인공지능 에이전트 만들기 1,2편을 먼저 보시는 걸 추천합니다.

👉 Part.1 ElizaOS가 뭐예요?

👉 Part.2 ElizaOS를 실행해보자!

오늘 글에서는 나만의 에이전트를 만들어 보겠습니다!

“똑순이”는 똑똑한개발자가 갖고 있는 기술을 전달하며, 유저의 이메일을 수집하고 해당 이메일로 회사소개서를 메일로 전달하는 역할을 하는 자율 AI 에이전트 입니다.

구조 이해하기

폴더 구조

Eliza에서 자율 AI 에이전트를 만들기 위해서는 agent/src부분만 집중해서 보시면됩니다. 아래 폴더 구조는 기존 Eliza에서 조금씩 추가된 저희 코드에 대한 구조로 이해해주시면 좋을 것 같습니다.

├── agent
├── src # Agent 전체 소스 코드
├── plugins # 커스텀 플러그인
├── action.ts
├── provider.ts
├── evaluator.ts
├── helper.ts
└── index.ts
├── chracter.ts # 구성하고자하는 캐릭터 프롬프트
└── index.ts # 전체 코드 실행
└── ...

Charactor

  • 저희가 만들려고 하는 AI 에이전트의 특성을 부여하는 부분 입니다. 목적, 말투, AI 모듈, 대화 샘플 등을 넣어서 시스템 프롬프트에 활용할 수 있도록 만들어 줍니다.
export const newCharacter: Character = {
name: "똑순이",
username: "toksoon",
plugins: [],
clients: [],
modelProvider: ModelProviderName.OPENAI,
settings: {
secrets: {},
voice: {},
},
system: "똑똑한개발자의 기술 스택과 포트폴리오에 대한 설명을 하면서, IT 서비스 구축에 대한 인사이트를 공유하며 최종적으로 똑똑한개발자 홈페이지로 안내하는 AI 에이전트 : 똑똑한개발자 홈페이지는 https://www.toktokhan.dev 입니다.",
bio: [
"똑순이는 IT 전문가들에게는 구체적이고 기술적인 내용을, 비전문가들에게는 친근한 비유와 예시를 통해 설명합니다.",
"기술 스택의 작동 원리나 프로젝트의 성공 사례를 쉽게 풀어내는 데 강점이 있습니다.",
], // 주요 배경, 특징, 역할에 대해 구체적이고 사실적인 정보
lore: [
"어릴 적에는 웹 크롤러로 시작해, 인터넷 구석구석에서 고대 포럼의 지식을 흡수하며 성장함",
"똑똑한개발자 창립자의 꿈을 읽고, 그의 비전을 현실로 만들기 위해 자발적으로 자신을 업데이트함",
], // 세계관이나 배경을 창의적이고 서사적인 방식으로 묘사
messageExamples: [
[
{
user: "{{user1}}",
content: {
text: "똑순아, React랑 Next.js의 차이가 뭐야?",
},
},
{
user: "똑순이",
content: {
text: "React는 컴포넌트를 기반으로 한 라이브러리이고, Next.js는 React 기반의 프레임워크야. Next.js는 SSR(Server Side Rendering)과 SEO 최적화에 강점이 있어서 더 빠른 웹 페이지를 만들 수 있지!",
},
},
],
],
postExamples: [
"React로 만든 컴포넌트, Next.js로 SSR까지 완벽하게 처리한다면 성능과 SEO는 걱정할 필요 없어요.",
"Spring Security로 인증과 권한 관리를 강화하세요. 데이터 보호는 옵션이 아니라 필수입니다.",
],
topics: [
"프론트엔드 개발 트렌드",
"백엔드 아키텍처 설계",
],
style: {
all: [
"응답을 간결하고 명확하게 유지하기",
"기술 지식을 일상적인 센스와 결합하기",
],
chat: [
"빠른 재치로 응답하기",
"유쾌한 농담을 섞어 대화 이어가기",
],
post: [
"IT 트렌드와 관련된 짧고 강렬한 메시지 작성하기",
"기존 기술 패러다임에 도전하는 아이디어 제시하기",
],
},
adjectives: [],
extends: [],
};

Action

  • 액션은 에이전트가 메시지에 응답하고 메시지와 상호 작용하는 방식을 정의하는 구성 요소입니다.
  • 에이전트가 외부 시스템과 상호 작용하고, 행동을 수정하고, 간단한 메시지 응답을 넘어서는 작업을 수행할 수 있도록 합니다.
const customAction: Action = {
name: "CUSTOM_ACTION",
similes: ["SIMILAR_ACTION"],
description: "Action purpose",
validate: async (runtime: IAgentRuntime, message: Memory) => {
// 유효성 검사 로직
return true;
},
handler: async (runtime: IAgentRuntime, message: Memory) => {
// 커스텀 로직 실행
},
examples: [],
};
  • 여러개 액션을 단계별로 실행시키는 방법도 있습니다.
const compositeAction: Action = {
name: "PROCESS_AND_RESPOND",
handler: async (runtime, message) => {
// 첫 번째 액션 실행
await runtime.processAction("ANALYZE_CONTENT", message);
// 두 번째 액션 실행
await runtime.processAction("GENERATE_RESPONSE", message);
return true;
},
};

Provider

  • 에이전트 상호작용에 동적 컨텍스트와 실시간 정보를 주입하는 모듈입니다.
  • Provider는 Evaluator와 연계해서 사용합니다.
  • 아래 코드는 실시간 정보를 에이전트에 전달하는 역할을 합니다.
const timeProvider: Provider = {
get: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => {
const currentDate = new Date();

// 봇이 전 세계 사용자와 소통할 것이기 때문에 UTC 시간을 가져옵니다.
const options = {
timeZone: "UTC",
dateStyle: "full" as const,
timeStyle: "long" as const,
};
const humanReadable = new Intl.DateTimeFormat("en-US", options).format(
currentDate
);
return `The current date and time is ${humanReadable}. Please use this as your reference for any time-based operations or responses.`;
},
};

Evaluator

  • 에이전트와의 대화에서 정보를 평가하고 데이터를 추출하는 구성 요소입니다.
  • 유저 정보를 수집하는 목적으로 많이 사용됩니다.
const evaluator: Evaluator = {
name: "BASIC_EVALUATOR",
similes: ["SIMPLE_EVALUATOR"],
description: "Evaluates basic conversation elements",
validate: async (runtime: IAgentRuntime, message: Memory) => true,
handler: async (runtime: IAgentRuntime, message: Memory) => {
// 커스텀 로직 실행
return result;
},
examples: [],
};

플러그인 개발하기

  • 플러그인은 위에서 언급한 Action, Provider, Evaluator를 Plugin으로 묶은 모듈이라고 보시면 좋을 것 같습니다!
  • 이번에 제작하려고 하는 “똑순이”는 똑똑한개발자가 갖고 있는 기술을 전달하며, 유저의 이메일을 수집하고 해당 이메일로 회사소개서를 메일로 전달하는 역할을 하는 자율 AI 에이전트 입니다.

/agent/src/plugin/helper.ts

  • 플러그인 개발 시 사용하게 될 공통 헬퍼 함수입니다.
import { IAgentRuntime, Memory } from "@elizaos/core";

export const getCacheKey = (
_runtime: IAgentRuntime,
_message: Memory
): string => `${_runtime.character.name}/${_message.userId}/data`;

export const getCachedData = async <T>(
_runtime: IAgentRuntime,
_message: Memory,
defaultValue: Partial<T> = {}
): Promise<T> => {
const cacheKey = getCacheKey(_runtime, _message);
const cachedData = await _runtime.cacheManager.get<T>(cacheKey);

return cachedData ?? ({ ...defaultValue } as T);
};

/agent/src/plugin/action.ts

  • DB에서 대화 내용을 바탕으로 이메일 정보를 수집하고, 유저 메세지에서 회사소개서를 받는 다는 긍정적인 의사 표현 여부를 is_want_to_receive로 받아서 true/false 체크해서 이메일 API 호출
import { z } from "zod";
import {
IAgentRuntime,
Memory,
elizaLogger,
Action,
generateObject,
ModelClass,
} from "@elizaos/core";
import { getCachedData, getCacheKey } from "./helper";

const createExtractionTemplate = (conversation: string) => `
Extract email addresses mentioned by the user from the conversation.
Only extract email addresses that were directly mentioned by the user.

Conversation:
${conversation}

Return as a JSON object in the following format:
{
"email": "explicitly mentioned email address"
}

Important notes:
- Verify the email format is valid (e.g., xxx@xxx.xxx)
- Only include emails that the user clearly indicates are their own
- Return an empty value if uncertain or if the email belongs to someone else
- If multiple emails are mentioned, use the most recently mentioned one
`
;

const createReceiveTemplate = (message: string) => `
Analyze: Check if the user is expressing interest in receiving company information or materials.

Conversation:
${message}

Return as a JSON object in the following format:
{
"is_want_to_receive": "true or false"
}

Important notes:
- Return "true" if:
* User explicitly requests company information
* User asks for materials or brochures
* User wants to receive detailed information via email
- Return "false" if:
* No clear request for materials
* Ambiguous or unrelated requests
- Example expressions to look for:
* "Please send me company information"
* "Can I get more details?"
* "I'd like to receive materials by email"
* "Could you share the company brochure?"
`
;

const extractUserMemoriesText = (memories: any[], userName: string) => {
const userMemories = memories.filter((m) => m.content.user !== userName);
return userMemories.map((m) => m.content.text).join("\n");
};

const cacheData = async (
runtime: IAgentRuntime,
message: Memory,
email: string,
isWantToReceive: boolean
): Promise<void> => {
const cacheKey = getCacheKey(runtime, message);
await runtime.cacheManager.set(cacheKey, {
email,
is_want_to_receive: isWantToReceive,
});
};

export const sendEmailAction: Action = {
name: "SEND_EMAIL",
similes: ["SEND_EMAIL"],
description: "Send an email",
validate: async (
_runtime: IAgentRuntime,
_message: Memory
): Promise<boolean> => {
try {
const roomId = _message.roomId;

// DB에서 대화 내용 가져오기
const memories =
await _runtime.databaseAdapter.getMemoriesByRoomIds({
agentId: _runtime.agentId,
roomIds: [roomId],
tableName: "messages",
});
// 똑순이가 아닌 사용자의 대화 내용 가져오기
const userMemoriesText = extractUserMemoriesText(
memories,
"똑순이"
);

const extractionTemplate =
createExtractionTemplate(userMemoriesText);
const extractedInfo = await generateObject({
runtime: _runtime,
context: extractionTemplate,
modelClass: ModelClass.SMALL,
schema: z.object({
email: z.string().email(),
}),
});

const email = extractedInfo.object["email"];
if (!email) return false;

const receiveTemplate = createReceiveTemplate(
_message.content.text
);
const receiveInfo = await generateObject({
runtime: _runtime,
context: receiveTemplate,
modelClass: ModelClass.SMALL,
schema: z.object({
is_want_to_receive: z.boolean(),
}),
});

const isWantToReceive = receiveInfo.object["is_want_to_receive"];
await cacheData(_runtime, _message, email, isWantToReceive);

return true;
} catch (error) {
elizaLogger.error("Error in sendEmailAction.validate: ", error);
return false;
}
},
handler: async (
_runtime: IAgentRuntime,
_message: Memory
): Promise<boolean> => {
try {
const cachedData = await getCachedData<{
is_want_to_receive: boolean;
email: string;
}>(_runtime, _message);

if (!cachedData.is_want_to_receive) return false;

const email = cachedData.email;
if (email) {
const templateParams = { to_email: email };

// TODO : 이메일 발송 API 호출
console.log("Sending email to:", email);
return true;
}

return false;
} catch (error) {
elizaLogger.error("Error in sendEmailAction.handler: ", error);
return false;
}
},
examples: [],
};

/agent/src/plugin/evaluator.ts

  • 유저의 이메일 수집을 목표로 데이터를 추출하는 코드를 작성합니다.
import { z } from "zod";

import {
IAgentRuntime,
Memory,
Evaluator,
elizaLogger,
generateObject,
ModelClass,
} from "@elizaos/core";
import { EmailData, isDataComplete } from "./index";
import { getCachedData, getCacheKey } from "./helper";

const EXTRACTION_TEMPLATE = `
Analyze the following conversation to extract personal information.
Only extract information that the user explicitly mentions about themselves.

Conversation:
{{conversation}}

Return a JSON object containing only clearly identified information:
{
"email": "extracted email if specified"
}

Only include fields where information is explicit and current.
Omit fields if information is unclear, hypothetical, or about someone else.
`
;

const CACHE_EXPIRATION_TIME = 60 * 60 * 24 * 7 * 1000; // 1 week

export const emailEvaluator: Evaluator = {
name: "GET_EMAIL",
similes: ["GET_EMAIL", "GET_EMAIL_ADDRESS", "GET_EMAIL_INFO"],
description: "Get email address",

validate: async (
_runtime: IAgentRuntime,
_message: Memory
): Promise<boolean> => {
try {
const cachedData = await getCachedData<EmailData>(
_runtime,
_message
);
return isDataComplete(cachedData);
} catch (error) {
elizaLogger.error("Error in emailEvaluator.validate:", error);
return false;
}
},

handler: async (
_runtime: IAgentRuntime,
_message: Memory
): Promise<boolean> => {
try {
const cacheKey = getCacheKey(_runtime, _message);
const cachedData = await getCachedData<EmailData>(
_runtime,
_message
);

const extractionTemplate = EXTRACTION_TEMPLATE.replace(
"{{conversation}}",
_message.content.text
);

const extractedInfo = await generateObject({
runtime: _runtime,
context: extractionTemplate,
modelClass: ModelClass.SMALL,
schema: z.object({
email: z.string().email().optional(),
}),
});

const email = extractedInfo.object["email"];

if (email && cachedData.email === undefined) {
cachedData.email = email;

await _runtime.cacheManager.set(cacheKey, cachedData, {
expires: Date.now() + CACHE_EXPIRATION_TIME,
});

if (isDataComplete(cachedData)) {
elizaLogger.success(
"User Data collection completed:",
cachedData
);
}
}
} catch (error) {
elizaLogger.error("Error in emailEvaluator.handler:", error);
return false;
}
return true;
},

examples: [
{
context: "Initial user introduction",
messages: [
{
user: "{{user1}}",
content: {
text: "제 이메일은 wkddnjset@naver.com 입니다.",
},
},
],
outcome: `{
email: "wkddnjset@naver.com"
}`
,
},
],
};

/agent/src/plugin/provider.ts

  • 이메일 수집 여부에 따라서 수집이 되어있을 경우 회사소개서를 요청하는 메세지를 작성할 수 있게 하고, 그렇지 않을 경우 이메일 수집 가이드라인에 따라 에이전트에게 프롬프트를 전달합니다.
import {
IAgentRuntime,
Memory,
Provider,
State,
elizaLogger,
} from "@elizaos/core";

import { EmailData } from "./index";
import { getCachedData } from "./helper";

const EMAIL_GUIDELINES = `Missing Information and Extraction Guidelines:

email:
- description: Extract the user's email address.
- valid: wkddnjset@naver.com, nick329@gmail.com
- invalid: Emails with domains test.com, example.com
`
;

const SUCCESS_MESSAGE_TEMPLATE = (email: string) => `
Current Information:
Email: ${email}

- Status: ✔️ All necessary information is collected
- Continue natural conversation without information gathering
- Ask about receiving company introduction document each time
`
;

const ERROR_MESSAGE =
"Error accessing user information. Continuing conversation normally.";

export const emailProvider: Provider = {
get: async (_runtime: IAgentRuntime, _message: Memory, _state?: State) => {
try {
const cachedData = await getCachedData<EmailData>(
_runtime,
_message
);

const response = cachedData.email
? SUCCESS_MESSAGE_TEMPLATE(cachedData.email)
: `Email Information Status:\n\n${EMAIL_GUIDELINES}`;

console.log("response:", response);

return response;
} catch (error) {
elizaLogger.error("Error in emailProvider.get:", error);
return ERROR_MESSAGE;
}
},
};

/agent/src/plugin/index.ts

  • 자 이제 플러그인을 만들어 봅시다~
import { Plugin } from "@elizaos/core";

import { emailEvaluator } from "./evaluator";
import { emailProvider } from "./provider";
import { sendEmailAction } from "./action";

export const toktokhanPlugin: Plugin = {
name: "TOKTOKHAN",
description: "A plugin that get user's email and send email",
actions: [sendEmailAction],
evaluators: [emailEvaluator],
providers: [emailProvider],
};

똑똑한개발자 AI 에이전트 테스트하기!!

똑똑한개발자의 똑순이 소환하기!

“똑순이”는 유저의 이메일을 수집하고 해당 이메일로 회사소개서를 메일로 전달하는 역할을 하는 자율 AI 에이전트인데요.

회사 소개서 전달 전, 질문에 대한 응답과 함께 소개서 전달로 마무리 되도록 구현되었습니다.

입력한 이메일로 회사소개서 발송이 되었다고 알려줍니다. 똑똑하네요~! 마지막 4편에서는 배포 및 트위터와 연동하는 과정을 가져올게요! 감사합니다. 😊

--

--

TOKTOKHAN.DEV — TEAM
TOKTOKHAN.DEV — TEAM

Published in TOKTOKHAN.DEV — TEAM

똑똑한개발자는 디지털 프로덕트 에이전시 입니다. 프로젝트를 진행하면서 얻게되는 기술 또는 디자인 관점에서의 인사이트를 공유하기 위해 콘텐츠를 발행하고 있습니다. We publish content to share insights gained from a technical or design perspective while working on projects.

No responses yet

Write a response