Контриб'юція
Практичний гайд з розширення AgentCore: нові канали, LLM-провайдери, простори імен і навчальні дані для намірів.
Додавання нового каналу
Канали — це плагіни Fastify з чергами BullMQ під капотом. WhatsApp і Telegram — еталонні реалізації: скопіюйте їхню структуру й адаптуйте транспорт.
1. Створіть каталог адаптера
src/channels/<channel-name>/
├── index.ts # Service class + factory
├── plugin.ts # Fastify plugin (registers routes + starts service)
├── handler.ts # Transport normalization + AgentTask creation
├── sender.ts # Outbound message formatting + delivery
├── queue.ts # BullMQ inbound/outbound workers
├── conversation-repository.ts # Prisma conversation persistence
└── types.ts # Channel-specific TypeScript types
2. Реалізуйте обробник
Обробник відповідає тільки за транспорт. Для кожного вхідного повідомлення:
- Знайдіть або створіть
UserіConversationза ідентифікатором користувача каналу - Збережіть вхідне
Messageвід користувача - Створіть
AgentTask - Поставте завдання в спільну чергу
agent-tasks - Передайте залежність для вихідних повідомлень, щоб воркер міг відправляти автоматичні або схвалені відповіді
Спільний воркер володіє інʼєкцією профілю, RAG, генерацією, виявленням prompt injection, ескалацією персони, класифікацією намірів, fallback за довірою, матрицею довіри, HITL-схваленням та маршрутизацією вихідних повідомлень. Див. ADR-001.
3. Зареєструйте плагін у src/app.ts
WhatsApp і Telegram-плагіни реєструються так (рядки 160–161 у app.ts):
import { whatsAppPlugin } from './channels/whatsapp/plugin.ts';
import { telegramPlugin } from './channels/telegram/plugin.ts';
// ...
void app.register(whatsAppPlugin, { prefix: '/api/v1' });
void app.register(telegramPlugin, { prefix: '/api/v1' });
Додайте свій канал у той самий блок:
import { slackPlugin } from './channels/slack/plugin.ts';
// ...
void app.register(slackPlugin, { prefix: '/api/v1' });
4. Додайте значення enum каналу у prisma/schema.prisma
enum ConversationChannel {
web
whatsapp
telegram
slack // ← add here
email
}
Потім зробіть міграцію:
npx prisma migrate dev --name add_slack_channel
5. Додайте змінні оточення
Додайте специфічні для каналу env-змінні у src/config.ts (Zod-схема) і .env.example.
Приклад — додавання Slack-каналу:
// src/config.ts — inside envSchema
SLACK_BOT_TOKEN: z.string().min(1).default('test-slack-bot-token'),
SLACK_SIGNING_SECRET: z.string().min(1).default('test-signing-secret'),
SLACK_APP_TOKEN: z.string().optional(),
Мінімальний diff — що міняється для нового каналу
Мінімальний diff додавання Slack виглядає так:
src/channels/slack/plugin.ts (новий файл — дзеркало telegram/plugin.ts):
export const slackPlugin: FastifyPluginAsync = async (app) => {
const redis = new Redis(config.REDIS_URL);
const { PrismaConversationRepository } = await import('./conversation-repository.ts');
const defaultDept = await prisma.department.findFirst({ select: { id: true } });
const conversations = defaultDept
? new PrismaConversationRepository(prisma, defaultDept.id)
: undefined;
const slackService = new SlackService({
config: {
botToken: config.SLACK_BOT_TOKEN,
signingSecret: config.SLACK_SIGNING_SECRET,
redisUrl: config.REDIS_URL,
},
redis,
...(conversations ? { conversations } : {}),
logger: app.log as unknown as import('pino').Logger,
memoryExtractionQueue: app.memoryExtractionQueue,
memoryExtractEveryN: config.MEMORY_EXTRACT_EVERY_N_MESSAGES,
agentTasksQueue: app.agentTasksQueue,
});
await slackService.start();
app.addHook('onClose', async () => { await slackService.stop(); await redis.quit(); });
app.post('/slack/events', ..., async (req, reply) => {
void slackService.handleEvent(req.body).catch(...);
return reply.send({ ok: true });
});
};
src/app.ts — два рядки:
+import { slackPlugin } from './channels/slack/plugin.ts';
...
+void app.register(slackPlugin, { prefix: '/api/v1' });
prisma/schema.prisma — один рядок у enum:
enum ConversationChannel {
telegram
+ slack
}
src/config.ts — нові env-змінні:
+ SLACK_BOT_TOKEN: z.string().min(1).default('test-slack-bot-token'),
+ SLACK_SIGNING_SECRET: z.string().min(1).default('test-signing-secret'),
Додавання нового LLM-провайдера
AgentCore використовує OpenAI SDK, який працює з будь-яким OpenAI-сумісним API. Підтримувані env-змінні визначені у src/config.ts:
| Змінна | Призначення | За замовчуванням |
|---|---|---|
OPENAI_API_KEY | API-ключ (обовʼязково) | — |
OPENAI_BASE_URL | Override base URL для сумісних провайдерів | OpenAI за замовчуванням |
OPENAI_MODEL | Модель chat completions | gpt-4o |
OPENAI_EMBEDDING_MODEL | Модель embeddings | text-embedding-3-small |
ANTHROPIC_API_KEY | Anthropic Claude (власний SDK, опційно) | — |
Ollama (локальний, API-ключ не потрібен)
OPENAI_API_KEY=ollama
OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_MODEL=llama3.1
OPENAI_EMBEDDING_MODEL=nomic-embed-text
Спочатку встановіть Ollama і підтягніть модель:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.1
ollama pull nomic-embed-text
Примітка. Embedding-моделі Ollama видають 768-вимірні вектори (nomic-embed-text), а не 1536. Якщо міняєте embedding-провайдера — оновіть розмір pgvector-колонки у
prisma/schema.prisma.
Anthropic Claude (через OpenAI-сумісний проксі)
Найпростіший підхід — OpenAI-сумісний проксі (наприклад, LiteLLM):
OPENAI_API_KEY=<your-anthropic-api-key>
OPENAI_BASE_URL=http://localhost:4000/v1 # LiteLLM proxy
OPENAI_MODEL=claude-3-7-sonnet-20250219
Також ANTHROPIC_API_KEY вже оголошений у src/config.ts як опційна змінна. Щоб використати нативний Anthropic SDK, додайте гілку вибору провайдера у src/knowledge/rag.ts, яка створює інстанс Anthropic, коли задано ANTHROPIC_API_KEY.
vLLM (self-hosted, OpenAI-сумісний)
OPENAI_API_KEY=<any-non-empty-string>
OPENAI_BASE_URL=http://your-vllm-host:8000/v1
OPENAI_MODEL=mistralai/Mistral-7B-Instruct-v0.3
OPENAI_EMBEDDING_MODEL=BAAI/bge-m3
Запуск vLLM:
python -m vllm.entrypoints.openai.api_server \
--model mistralai/Mistral-7B-Instruct-v0.3 \
--host 0.0.0.0 --port 8000
Azure OpenAI
Azure має інший формат ендпоінта і вимагає api-version у кожному запиті. Скористайтесь Azure-підтримкою в openai SDK:
OPENAI_API_KEY=<your-azure-api-key>
OPENAI_BASE_URL=https://<resource-name>.openai.azure.com/openai/deployments/<deployment-name>
OPENAI_MODEL=gpt-4o # must match your Azure deployment name
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
У src/knowledge/rag.ts створіть клієнт з Azure-креденшлами:
import { AzureOpenAI } from 'openai';
const openai = new AzureOpenAI({
apiKey: config.OPENAI_API_KEY,
endpoint: config.OPENAI_BASE_URL,
apiVersion: '2024-12-01-preview',
});
Кастомний провайдер (без OpenAI-сумісності)
Для провайдерів без OpenAI-сумісного API:
- Створіть клас, який реалізує той самий інтерфейс, що й
OpenAiRagPipelineуsrc/knowledge/rag.ts - Додайте логіку вибору провайдера у фабрику pipeline
- Переконайтесь, що embedding-модель видає 1536-вимірні вектори (дефолт pgvector-колонки) — або оновіть схему і повторіть міграцію
Додавання нового простору імен (відділу)
Простір імен — це AI-конфіг відділу: власний system prompt, персона, база знань, приклади намірів. Цей чекліст охоплює повний шлях від нуля до першої відповіді бота.
Новий код, що читає або змінює дані відділу, має відповідати патерну forDepartment().
Крок 1 — Створіть або визначте відділ
Відділи можна засідити або створити через API. Збережіть id відділу як DEPT_ID.
curl -X POST http://localhost:3000/api/v1/departments \
-H 'Authorization: Bearer <admin-token>' \
-H 'Content-Type: application/json' \
-d '{"name": "Finance", "slug": "finance", "color": "green"}'
Крок 2 — Створіть простір імен
curl -X POST http://localhost:3000/api/v1/namespaces \
-H 'Authorization: Bearer <admin-token>' \
-H 'Content-Type: application/json' \
-d '{
"name": "finance",
"departmentId": "<DEPT_ID>",
"systemPrompt": "You are a financial assistant for Acme Corp employees. Answer questions about payroll, expenses, and budget processes. You do not provide tax advice.",
"persona": {
"language": "en",
"style": { "formality": 80 },
"boundaries": [
"Do not provide tax advice",
"Do not share salary information of other employees"
]
}
}'
Збережіть поверненний id як NS_ID.
Крок 3 — Створіть базу знань
curl -X POST http://localhost:3000/api/v1/knowledge/bases \
-H 'Authorization: Bearer <admin-token>' \
-H 'Content-Type: application/json' \
-d '{"departmentId": "<DEPT_ID>", "name": "Finance KB"}'
Крок 4 — Завантажте документи
Завантажте PDF, DOCX або TXT-файли. Ingestion pipeline автоматично розбиває їх на чанки, створює embeddings та індексує:
curl -X POST http://localhost:3000/api/v1/knowledge/upload \
-H 'Authorization: Bearer <admin-token>' \
-F 'knowledgeBaseId=<KB_ID>' \
-F 'title=Expense policy 2024' \
-F 'file=@./docs/expense-policy-2024.pdf'
Для масового завантаження повторіть команду для кожного файлу або напишіть shell-цикл:
for f in ./finance-docs/*.pdf; do
curl -X POST http://localhost:3000/api/v1/knowledge/upload \
-H 'Authorization: Bearer <admin-token>' \
-F 'knowledgeBaseId=<KB_ID>' \
-F "title=$(basename "$f")" \
-F "file=@$f"
done
Крок 5 — Налаштуйте персону (опційно)
Оновіть system prompt і формальність простору імен після того, як переглянете перші відповіді:
curl -X PATCH http://localhost:3000/api/v1/namespaces/<NS_ID> \
-H 'Authorization: Bearer <admin-token>' \
-H 'Content-Type: application/json' \
-d '{"systemPrompt": "...", "persona": {"style": {"formality": 30}}}'
Крок 6 — Засідіть початкові приклади намірів
Прокачайте класифікатор намірів стартовими прикладами фраз (див. Додавання нових прикладів намірів нижче).
Крок 7 — Тестуйте через канал
Відправте тестове повідомлення через будь-який активний канал (WhatsApp, Telegram) або через API. Дивіться у app.log класифікацію намірів і confidence-значення.
Простір імен працює, коли:
- Повідомлення маршрутизуються у правильний простір імен на основі призначення відділу
- RAG-pipeline повертає обґрунтовану відповідь з посиланнями на контент бази знань
- Відповіді з низькою впевненістю коректно тригерять HITL-ревʼю
Додавання нових прикладів намірів
Приклади намірів використовує класифікатор на векторній схожості у src/knowledge/intent-classifier.ts. Більше якісних прикладів — краща точність класифікації.
Через API (одна фраза)
curl -X POST http://localhost:3000/api/v1/intents/examples \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"namespaceId": "<NS_ID>",
"intentName": "leave_policy",
"phrase": "How many vacation days am I entitled to?"
}'
Embedding генерується й зберігається автоматично.
Через скрипт масового сіду
Скористайтесь scripts/seed-intents.ts для імпорту з JSON-файлу. Скрипт підтримує два формати:
Формат 1 — intent map:
{
"intentMap": {
"leave_policy": ["How many vacation days?", "When does PTO reset?"],
"expense_reimbursement": {
"examplePhrases": ["How do I submit expenses?", "What's the reimbursement limit?"]
}
}
}
Формат 2 — golden Q&A:
{
"goldenQA": [
{ "intent": "leave_policy", "question": "Can I carry over unused vacation?" },
{ "intent": "payroll", "question": "When is payday?" }
]
}
Запустіть сід:
npx tsx scripts/seed-intents.ts \
--input ./data/finance-intents.json \
--namespace finance
Скрипт батчить embeddings (32 фрази на API-виклик), дедуплікує рядки та показує прогрес-бар.
З реальних розмов
Найкращі приклади намірів беруться з реальних розмов з користувачами. Скористайтесь scripts/analyze-chats.ts, щоб витягти їх автоматично:
# Analyze a folder of conversation exports
npx tsx scripts/analyze-chats.ts \
--input ./chats/finance-q1/ \
--output ./analysis/finance-q1/ \
--format json \
--batch-size 5 \
--namespace finance
Аналізатор:
- Читає розмови у JSON, CSV або TXT
- Запускає LLM-аналіз кожної розмови, щоб витягти намір клієнта та приклади фраз
- Зберігає
intentMap.jsonіgoldenQA.jsonу вихідний каталог - Сідить витягнуті фрази одразу в pgvector-таблицю
intent_examples
Типовий воркфлоу для нового простору імен:
# 1. Export past conversations from your CRM / chat platform
cp /path/to/crm-export/*.json ./chats/finance-q1/
# 2. Run the analyzer
npx tsx scripts/analyze-chats.ts \
--input ./chats/finance-q1/ \
--output ./analysis/finance-q1/ \
--namespace finance
# 3. Review the output
cat ./analysis/finance-q1/intentMap.json | jq 'keys'
# 4. Edit / curate if needed, then re-seed
npx tsx scripts/seed-intents.ts \
--input ./analysis/finance-q1/intentMap.json \
--namespace finance
Поради щодо якості:
- Цільтесь на ≥20 прикладів фраз на намір для надійної класифікації
- Варіюйте формулювання — додавайте формальні та неформальні варіанти
- Приберіть дублікати та майже-дублікати перед сідом
- Повторно запускайте сід після курування — він працює через upsert, тому повторний запуск безпечний
Якість коду
Перед відправкою змін:
npm run typecheck # TypeScript strict mode check
npm run lint:fix # ESLint auto-fix
npm run format # Prettier formatting
npm test # Vitest unit + integration tests
Конвенції проекту
- TypeScript strict mode — весь код типізований
- Zod-валідація — усі env-змінні та API-inputs валідуються через Zod
- Fastify-плагіни — групи маршрутів — це Fastify-плагіни, зареєстровані у
src/app.ts - BullMQ — асинхронна робота йде через черги (не напряму в request-хендлерах)
- Prisma — весь доступ до БД через Prisma-клієнт
- ESM — проект використовує ES-модулі (
"type": "module")