Вклад в проект
Практическое руководство по расширению AgentCore: новые каналы, LLM-провайдеры, namespaces и обучающие данные для intents.
Добавление нового канала
Каналы — это плагины 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. Реализуйте handler
Handler должен отвечать только за транспорт. Для каждого входящего сообщения:
- Найдите или создайте
UserиConversationпо идентификатору пользователя канала. - Сохраните входящее
Messageот пользователя. - Создайте
AgentTask. - Положите задачу в общую очередь
agent-tasks. - Пробросьте outbound-зависимость, чтобы воркер мог отправлять авто-пропущенные или одобренные ответы.
Общий воркер отвечает за инъекцию профиля, RAG, генерацию, маршрутизацию embeddings, эскалацию персоны, классификацию intents, fallback по уверенности, матрицу доверия, HITL-утверждение и outbound-маршрутизацию. См. 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. Добавьте переменные окружения
Пропишите переменные окружения, специфичные для канала, в 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(),
Проход по диффу — что меняется при добавлении канала
Минимальный дифф для нового канала 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 — новые переменные окружения:
+ 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, который работает с любым API, совместимым с OpenAI. Поддерживаемые переменные окружения определены в src/config.ts:
| Переменная | Назначение | По умолчанию |
|---|---|---|
OPENAI_API_KEY | API-ключ (обязательно) | — |
OPENAI_BASE_URL | Переопределение base URL для совместимых провайдеров | дефолт OpenAI |
OPENAI_MODEL | Модель для chat completion | 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. Если меняете провайдера embeddings — обновите размерность столбца pgvector в
prisma/schema.prisma.
Anthropic Claude (через прокси, совместимый с OpenAI)
Простейший способ — прокси с OpenAI-совместимым API (например, LiteLLM):
OPENAI_API_KEY=<your-anthropic-api-key>
OPENAI_BASE_URL=http://localhost:4000/v1 # LiteLLM proxy
OPENAI_MODEL=claude-3-7-sonnet-20250219
Вариант через нативный SDK: 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. - Добавьте выбор провайдера в фабрику пайплайнов.
- Убедитесь, что embedding-модель выдаёт 1536-мерные векторы (дефолтный столбец pgvector) — иначе обновите схему и мигрируйте заново.
Добавление нового namespace (отдел)
Namespace — это AI-конфигурация отдела: свой system prompt, персона, база знаний и примеры intents. Ниже чеклист полного пути от нуля до первого ответа бота.
Новый код, который читает или пишет данные на уровне отдела, должен следовать шаблону маршрута 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 — Создайте namespace
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-пайплайн автоматически разобьёт на чанки, сгенерирует 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 и настройки формальности namespace после того, как увидите первые ответы:
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 — Стартовые примеры intents
Прогрейте классификатор intents готовыми примерами фраз (см. Добавление новых примеров intents ниже).
Шаг 7 — Проверка канала
Отправьте тестовое сообщение через любой активный канал (WhatsApp, Telegram) или через API. Смотрите app.log — там видны оценки классификатора intents и значения confidence.
Namespace считается активным, когда:
- Сообщение роутится в правильный namespace по привязке отдела.
- RAG-пайплайн возвращает обоснованный ответ со ссылкой на контент из базы знаний.
- Ответы с низкой уверенностью корректно уходят на HITL-ревью.
Добавление новых примеров intents
Примеры intents используются классификатором векторного сходства в 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 — карта intents:
{
"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), апсертит записи (без дубликатов) и показывает прогресс-бар.
Из реальных данных переписок
Лучшие примеры intents — из реальных диалогов с пользователями. Используйте 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-анализ по каждому диалогу — выделяет intents клиента и примеры фраз.
- Пишет
intentMap.jsonиgoldenQA.jsonв выходной каталог. - Записывает извлечённые фразы прямо в таблицу
intent_examples(pgvector).
Типовой флоу для нового namespace:
# 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 фраз на один intent для надёжной классификации.
- Варьируйте формулировки — добавляйте и формальные, и неформальные варианты.
- Убирайте дубликаты и почти-дубликаты перед сидом.
- После курирования перезапустите сид-скрипт; он идемпотентный, так что повторный запуск безопасен.
Качество кода
Перед тем как отправить изменения:
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 — все переменные окружения и входные данные API валидируются через Zod.
- Плагины Fastify — группы маршрутов оформлены как плагины Fastify, зарегистрированные в
src/app.ts. - BullMQ — асинхронная работа идёт через очереди, а не напрямую в обработчиках запросов.
- Prisma — весь доступ к БД через Prisma-клиент.
- ESM — в проекте используются ES-модули (
"type": "module").