Архитектура AI Orchestrator
@lowcode/ai-orchestrator
@lowcode/ai-orchestrator — это библиотека‑оркестратор AI‑запросов для Lowcode Platform. Она находится «между» редактором и конкретными AI‑провайдерами и решает задачи:
- формирование промпта на основе DSL (
AppSchema), контекста редактора и текста запроса пользователя; - нормализация вызова моделей в единый формат (
AiModelRequest/AiModelResponse); - разбор ответа модели (JSON → набор типизированных операций над DSL
AiOperation[]); - аккуратное применение этих операций к
AppSchemaи выдача нового DSL + текстового объяснения.
Промпт учитывает актуальные глобальные контексты выражений, включая route.* (path + params + query) и event.* (внутри обработчиков), а также поддерживает navigate с params и query.
Библиотека опирается на:
@lowcode/dsl— описание схемы приложения (AppSchema, ComponentNode и т.п.);@lowcode/shared-types— общий контракт AI (AiAssistRequest,AiAssistResponse,AiModelRequest,AiModelResponse,AiOperation, подсказки контекста и т.д.).
Хост (обычно backend @lowcode/api) предоставляет только:
- реализацию
callModel(как сходить в OpenAI / AI Tunnel / локальную модель); - выбор провайдера (
provider) и модели (model); - логгирование, авторизацию и остальные инфраструктурные вещи.
1. Роль ai-orchestrator в общей архитектуре
Высокоуровневый сценарий работы AI‑ассистента:
builder-web (UI, чат пользователя)
→ HTTP POST /ai/assist (apps/api)
→ AiService (NestJS) собирает AiAssistRequest
→ @lowcode/ai-orchestrator.assist(input, callModel)
→ buildAssistModelRequest(...) # формирование промпта
→ callModel(modelRequest) # вызов AI-провайдера
→ parseAssistModelResponse(modelResponse) # разбор JSON-ответа
→ applyAiOperations(appSchema, operations) # операции над DSL
→ ответ AiAssistResponse
→ builder-web обновляет DSL и показывает текстовое объяснение
Ключевые идеи:
- Чистота: ai-orchestrator ничего не знает ни про HTTP, ни про SDK конкретных моделей. Вся сеть — во внешнем
callModel. - DSL‑центричность: библиотека работает с DSL и над DSL — промпт строится вокруг
AppSchema, результатом являются операции надAppSchema. - Унификация: независимо от того, используем ли мы прямой OpenAI‑API, AI Tunnel или локальную модель, на входе/выходе у ai-orchestrator всегда один и тот же контракт.
2. Структура пакета
packages/ai-orchestrator/
├─ src/
│ ├─ index.ts # Публичный API: re-export assist()
│ └─ assist/
│ ├─ assist.ts # Главный use-case: функция assist(...)
│ ├─ buildPrompt.ts # Формирование AiModelRequest (messages[]) из AiAssistRequest
│ ├─ parseResponse.ts # Разбор AiModelResponse → AiOperation[] + explanation
│ └─ applyOperations.ts # Применение AiOperation[] к AppSchema
├─ tests/
│ └─ assist/
│ ├─ assist.test.ts # Интеграционный тест пайплайна assist()
│ ├─ buildPrompt.test.ts # Юнит-тест формирования промпта
│ ├─ parseResponse.test.ts # Юнит-тест разбора ответа
│ └─ applyOperations.test.ts # Юнит-тест применения операций к DSL
├─ tsconfig.json # Основной TS-конфиг пакета
├─ tsconfig.spec.json # TS-конфиг для Jest-тестов
├─ tsconfig.docs.json # TS-конфиг для Typedoc
├─ typedoc.json # Конфиг Typedoc (entryPoints + out)
├─ jest.config.cjs # Конфигурация Jest
└─ package.json # Метаданные пакета и скрипты
Основные слои:
- assist‑слой (
assist.ts) — высокоуровневый use‑case: запрос → model → операции → новый DSL; - prompt‑слой (
buildPrompt.ts) — сборка system/user‑сообщений для модели в OpenAI‑подобном формате; - parser‑слой (
parseResponse.ts) — разбор текстового ответа модели в структурированный формат; - applier‑слой (
applyOperations.ts) — применение операций кAppSchemaбез знания о конкретном UI.
3. Ключевые типы и сущности
Типы находятся в @lowcode/shared-types/src/ai.ts. Ниже — концептуальный обзор.
3.1. AiAssistRequest / AiAssistResponse
Высокоуровневый контракт между builder-web и backend (а внутри backend’а — между API и ai-orchestrator):
interface AiAssistRequest {
/** Текст запроса пользователя (чат-ввод) */
prompt: string;
/** Текущее приложение целиком — основной источник истины для AI */
appSchema: AppSchema;
/** Дополнительный контекст проекта/страницы */
projectId?: string;
pageId?: string;
/** Подсказки о selection и состоянии редактора */
hints?: AiEditorContextHints;
/** Выбранный провайдер и модель (UI в builder-web) */
provider: 'direct' | 'aiTunnel' | 'local';
model: string;
/** Опциональные идентификаторы для логгирования и трейсинга */
requestId?: string;
}
interface AiAssistResponse {
/** Новый AppSchema после применения операций (если они были) */
updatedAppSchema?: AppSchema;
/** Сырые операции, предложенные моделью */
operations?: AiOperation[];
/** Текстовое объяснение, что было сделано / почему */
explanation?: string;
/** Информация об ошибке, если что-то пошло не так */
error?: AiError;
}
AiAssistRequest собирается на стороне API из данных, пришедших от builder-web. AiAssistResponse возвращается обратно в редактор, где уже принимается решение — обновлять DSL, показывать diff, проигрывать изменения пошагово и т.п.
3.2. AiModelRequest / AiModelResponse
Низкоуровневый контракт между ai-orchestrator и конкретным провайдером модели. Формат максимально близок к OpenAI‑подобному chat‑API:
interface AiModelRequest {
provider: AiProviderType; // 'direct' | 'aiTunnel' | 'local'
model: string;
messages: AiChatMessage[]; // system + user (и далее, если нужно)
maxTokens?: number;
temperature?: number;
requestId?: string;
traceId?: string;
tools?: AiToolDefinition[]; // зарезервировано под function calling
toolChoice?: AiToolChoice;
}
interface AiModelResponse {
messages: AiChatMessage[]; // обычно одно assistant-сообщение
usage?: AiUsage; // опциональная статистика токенов
toolCalls?: AiToolCall[]; // под function calling / tools
}
Эти типы используются внутри assist() и buildAssistModelRequest(). Реальная работа с сетью происходит в переданном извне callModel(req).
3.3. AiOperation — операции над DSL
AiOperation описывает элементарное изменение в DSL. Типы операций ориентированы на реальные сценарии в редакторе:
type AiOperationKind =
| 'explainSelection'
| 'addComponent'
| 'moveComponent'
| 'updateComponentProps'
| 'updateLayout'
| 'deleteComponent'
| 'addPage'
| 'updatePage'
| 'updateAppShell'
| 'deletePage'
| 'addDataSource'
| 'updateDataSource'
| 'deleteDataSource'
| 'addEventHandler'
| 'updateEventHandler'
| 'deleteEventHandler'
| 'addAppStateVariable'
| 'updateAppStateVariable'
| 'deleteAppStateVariable'
| 'addPageStateVariable'
| 'updatePageStateVariable'
| 'deletePageStateVariable'
| 'fixValidationErrors'
| 'other';
Примеры конкретных форм (упрощённо):
interface AiAddComponentOperation {
kind: 'addComponent';
pageId: string;
parentId: string;
component: ComponentNode; // готовый DSL-компонент
}
interface AiMoveComponentOperation {
kind: 'moveComponent';
pageId: string;
componentId: string;
parentId?: string;
insertPosition?: 'auto' | 'start' | 'end' | { beforeComponentId?: string; afterComponentId?: string };
}
interface AiUpdateComponentPropsOperation {
kind: 'updateComponentProps';
pageId: string;
componentId: string;
propsPatch?: Record<string, PropValue>;
layoutPatch?: Record<string, PropValue>;
stylePatch?: Record<string, PropValue>;
}
interface AiUpdateLayoutOperation {
kind: 'updateLayout';
pageId: string;
componentId: string;
layoutPatch: Record<string, PropValue>;
}
interface AiUpdateAppShellOperation {
kind: 'updateAppShell';
shell: ComponentNode | null;
}
interface AiAddDataSourceOperation {
kind: 'addDataSource';
dataSource: DataSource;
}
interface AiAddEventHandlerOperation {
kind: 'addEventHandler';
handler: EventHandler;
}
Полный набор операций описан и типизирован в @lowcode/shared-types.
3.4. Нормализация style/layout патчей
AI не всегда аккуратно разводит визуальные и layout‑поля, поэтому applyOperations.ts перед применением операций выполняет нормализацию:
- все известные визуальные ключи (
backgroundColor,borderRadius,fontSize,textAlign,classNameи т.п.) изpropsPatchперекладываются вstylePatch; - layout‑ключи (
width,height,gap,padding,alignItems,justifyContent,flexDirection, ...) перекладываются вlayoutPatch; - если среди layout‑ключей встречаются flex‑настройки (
flexDirection,alignItems,justifyContent,flexGrowи т.д.), аdisplayне указан, автоматически подставляетсяdisplay: "flex". Это отражает поведение Quick‑меню PropertiesPanel, где есть явный Flex toggle: безdisplay: flexостальные flex‑свойства не работают.
Те же правила явно проговариваются в системном промпте (buildPrompt.ts), чтобы модель по возможности сразу возвращала корректные stylePatch и layoutPatch. Нормализация служит «страховкой», если ответ не соответствует требованиям.
Дополнительные нормализации в applyOperations.ts:
- для
DataSourceприводитсяtype → kindиconfig.value → config.data(дляkind="static"); - для встроенных компонентов отбрасываются неизвестные props (исключение —
BlockInstance, где пропсы определяет схема блока).
3.5. AiEditorContextHints — подсказки контекста редактора
Чтобы модель могла корректно интерпретировать фразы вроде «эта кнопка», «этот текст», ai-orchestrator принимает подсказки из редактора:
interface AiSelectionHint {
kind: 'component' | 'page' | 'app' | 'expression' | 'dataSource' | 'state';
pageId?: string;
componentId?: string;
propName?: string;
propSection?: 'props' | 'layout' | 'style' | 'state' | 'dataSource' | 'event';
dslPath?: string; // запасной вариант: путь внутри AppSchema
}
interface AiEditorContextHints {
primarySelection?: AiSelectionHint;
}
Редактор сам решает, что считать primarySelection (например, последнюю отмеченную ноду в Canvas или текущий фокус в PropertiesPanel). Эта информация сериализуется в user‑payload и попадает в промпт.
3.6. CallModelFn — адаптер провайдера моделей
ai-orchestrator не ходит в сеть сам. Вместо этого в assist() ему передают функцию:
export type CallModelFn = (req: AiModelRequest) => Promise<AiModelResponse>;
На уровне API её можно реализовать через:
- официальный SDK OpenAI;
- совместимый SDK, работающий через AI Tunnel (изменяется только
baseURLиapiKey); - локальный HTTP-сервис, который принимает
AiModelRequestи отвечаетAiModelResponse; - заглушку для тестов.
4. Главный use-case: функция assist()
Входная точка пакета — функция assist():
export async function assist(
input: AiAssistRequest,
callModel: CallModelFn,
): Promise<AiAssistResponse> {
const modelRequest = buildAssistModelRequest(input);
const modelResponse = await callModel(modelRequest);
const { operations, explanation } = parseAssistModelResponse(modelResponse);
const updatedAppSchema = applyAiOperations(input.appSchema, operations);
return {
updatedAppSchema,
operations,
explanation,
};
}
Пайплайн:
-
Формирование промпта —
buildAssistModelRequest(input):- собирает
system‑сообщение: роль ассистента, формат ответа, правила; - собирает
user‑сообщение: JSON‑payload с полямиprompt,appSchema,projectId,pageId,hints; - возвращает
AiModelRequestс заполненнымиmessages,provider,model,requestId.
- собирает
-
Вызов модели —
callModel(modelRequest):- реализуется на стороне API (см. раздел про интеграцию ниже);
- на этом шаге выбирается, использовать ли прямой OpenAI, AI Tunnel или локальную модель.
-
Разбор ответа —
parseAssistModelResponse(modelResponse):- ищет первое
assistant‑сообщение вmessages; - пытается распарсить
contentкак JSON с полями{ operations, explanation }; - если JSON не парсится — воспринимает
contentкак plain‑текстexplanationи возвращает пустой список операций; - добавляет защиту от
null/отсутствия сообщений.
- ищет первое
-
Применение операций к DSL —
applyAiOperations(appSchema, operations):- создаёт копию исходного
AppSchema(immutability); - последовательно применяет операции
AiOperation(patch‑логика); - возвращает новый
AppSchema, который можно сразу отрисовывать в превью.
- создаёт копию исходного
Результирующий AiAssistResponse уже удобно отдавать в builder-web, где можно либо прямо заменить DSL, либо показать diff.
5. Формирование промпта: buildPrompt.ts
Файл assist/buildPrompt.ts отвечает за создание AiModelRequest.
Основные принципы:
-
в
system‑сообщении задаются правила работы ассистента:- он работает в контексте low-code‑редактора;
- оперирует строго структурированным DSL (
AppSchema); - обязан возвращать JSON с полями
operationsиexplanation; - не имеет права самовольно придумывать несуществующие типы компонент или пропсы;
- знает про
orderвComponentNodeи используетmoveComponentдля перестановки детей; - должен учитывать
hints.primarySelectionкак «то, про что говорит пользователь» по умолчанию.
-
в
user‑сообщении содержится один JSON‑объект со структурой примерно вида:{
"prompt": "Сделай эту кнопку более круглой",
"projectId": "...",
"pageId": "page-1",
"appSchema": {
/* текущее DSL-приложение целиком */
},
"hints": {
"primarySelection": {
"kind": "component",
"pageId": "page-1",
"componentId": "btn-1",
"propSection": "props",
},
},
}
На уровне AiModelRequest никакой бизнес‑логики больше не происходит. Всё, что связано с выбором модели и провайдера, приходит из AiAssistRequest.
6. Разбор ответа: parseResponse.ts
Файл assist/parseResponse.ts концентрируется на безопасном разборе ответа модели.
Алгоритм:
- Берётся первое сообщение с
role: 'assistant'изAiModelResponse.messages. - Если сообщение отсутствует — возвращается
{ operations: [], explanation: undefined }. - Пытается распарсить
message.contentкак JSON.-
При успехе:
- ожидаются поля
operations(массив) иexplanation(строка); - при отсутствии
operationsподставляется пустой массив.
- ожидаются поля
-
При ошибке парсинга:
- содержимое поля
contentвоспринимается как plain‑текстexplanation; - операции считаются пустыми.
- содержимое поля
-
Такой дизайн позволяет:
- на ранних этапах (когда промпт ещё сырой) всё равно выдавать пользователю полезный текстовый ответ;
- постепенно ужесточать контракт и схему JSON без ломания API.
7. Применение операций к DSL: applyOperations.ts
Файл assist/applyOperations.ts отвечает за применение AiOperation[] к AppSchema.
Особенности реализации:
- входящий
AppSchemaне мутируется — всегда создаётся новая структура; - операции применяются по очереди, в одном проходе;
- при невозможности применить конкретную операцию (например, компонент не найден) — операция пропускается, остальные продолжают выполняться.
Поддерживаются, в частности, следующие операции:
-
addComponent -
ищется
PageSchemaпоpageId; -
ищется родительский
ComponentNodeпоparentIdв дереве страницы; -
в
childrenродителя добавляется новыйComponentNode. -
moveComponent -
ищется компонент и его родитель по
componentId; -
компонент удаляется из текущего родителя и вставляется в
parentId(если задан) или в текущего родителя; -
позиция вставки контролируется
insertPosition(start/end/before/after/auto). -
если в детях уже используется
order, он нормализуется под новый порядок. -
updateComponentProps- ищется компонент по
pageId + componentId; props,layoutиstyleпатчатся shallow‑merge’ом (новые значения заменяют старые).
- ищется компонент по
-
updateLayout- аналогично, но патчится
layout.
- аналогично, но патчится
-
addDataSource- новый
DataSourceдобавляется вappSchema.dataSources; - при отсутствии массива
dataSourcesсоздаётся новый.
- новый
-
updateDataSource- ищется существующий источник по
dataSourceId; - поверх него применяется shallow‑patch из
patch.
- ищется существующий источник по
-
addEventHandlerEventHandlerдобавляется кappSchema.eventHandlers(для компонентов или страниц);- создаётся массив, если его не было.
Логика deliberately простая — основная ответственность за корректность операций лежит на промпте и модели. При необходимости в будущем сюда можно добавить строгую валидацию / отчёт о неудачных операциях.
8. Интеграция с backend API (@lowcode/api)
Типичный сценарий использования ai-orchestrator в NestJS‑сервисе:
@Injectable()
export class AiService {
constructor(/* зависимости для реальных SDK провайдеров */) {}
private async callModel(req: AiModelRequest): Promise<AiModelResponse> {
// Здесь реализуется адаптер: OpenAI, AI Tunnel или локальный HTTP-сервис.
// Пример для AI Tunnel (псевдокод):
//
// const client = new OpenAI({
// apiKey: process.env.AI_TUNNEL_API_KEY,
// baseURL: 'https://api.aitunnel.ru/v1/',
// });
//
// const result = await client.chat.completions.create({
// model: req.model,
// messages: req.messages,
// max_tokens: req.maxTokens,
// temperature: req.temperature,
// });
//
// return {
// messages: [
// {
// role: result.choices[0].message.role,
// content: result.choices[0].message.content,
// },
// ],
// usage: {
// promptTokens: result.usage?.prompt_tokens,
// completionTokens: result.usage?.completion_tokens,
// totalTokens: result.usage?.total_tokens,
// },
// };
throw new Error('callModel не реализован');
}
async assist(request: AiAssistRequest): Promise<AiAssistResponse> {
return assist(request, (modelReq) => this.callModel(modelReq));
}
}
Дальше поверх этого сервиса строится контроллер AiController с эндпоинтом вроде:
POST /ai/assist
который:
- принимает DTO (близкий к
AiAssistRequest); - достаёт
AppSchema(из тела запроса или из БД поprojectId/version); - вызывает
aiService.assist(); - возвращает
AiAssistResponseредактору.
9. Интеграция с builder-web
На стороне builder-web ai-orchestrator напрямую не используется — редактор разговаривает с backend API.
Типичный UX‑флоу для AI‑панели в редакторе:
-
Пользователь выделяет компонент/страницу и нажимает на кнопку AI‑ассистента.
-
Открывается чат‑панель, пользователь вводит запрос (
prompt). -
Редактор собирает:
- текущий
AppSchema(или берёт последнюю сохранённую версию); projectId/pageId;hints.primarySelection(тип selection, компонент, секция и т.п.);- выбранного провайдера и модель (UI‑селекторы: direct / aiTunnel + конкретная модель).
- текущий
-
Всё это отправляется в API в формате
AiAssistRequest. -
API через
@lowcode/ai-orchestratorвозвращаетAiAssistResponse. -
Редактор:
- подставляет
updatedAppSchemaв превью; - при желании показывает diff (список операций);
- отображает
explanationдля пользователя.
- подставляет
На уровне ai-orchestrator вся эта логика уже абстрагирована: библиотека видит только один чистый объект AiAssistRequest.
10. Тестирование
Пакет @lowcode/ai-orchestrator покрыт Jest‑тестами, которые повторяют структуру src/assist:
-
assist.test.ts- интеграционный тест для функции
assist():- мокает
callModelи возвращает заранее подготовленныйAiModelResponse; - проверяет, что на выходе получаем корректный
updatedAppSchema, список операций иexplanation; - убеждается, что
callModelвызывается ровно один раз.
- мокает
- интеграционный тест для функции
-
buildPrompt.test.ts- проверяет, что
buildAssistModelRequest():- формирует 2 сообщения (
system+user); - корректно сериализует
AiAssistRequestв JSON‑payload user‑сообщения; - прокидывает
provider,model,requestIdвAiModelRequest.
- формирует 2 сообщения (
- проверяет, что
-
parseResponse.test.ts- проверяет разбор JSON‑ответа и fallback в plain‑текст:
- корректный JSON → извлекаются
operationsиexplanation; - невалидный JSON →
operations = [],explanation = content.
- корректный JSON → извлекаются
- проверяет разбор JSON‑ответа и fallback в plain‑текст:
-
applyOperations.test.ts- тестирует применение основных операций
AiOperationк простомуAppSchema:- добавление компонента;
- обновление props/layout/style;
- добавление и обновление
dataSources; - добавление
eventHandlers; - проверка immutability (исходный DSL не модифицируется).
- тестирует применение основных операций
Запуск тестов:
pnpm --filter @lowcode/ai-orchestrator test
11. План развития
Ориентировочный roadmap для @lowcode/ai-orchestrator:
-
Расширение набора AiOperation
- более точные операции для
appState/pageState; - отдельные операции для
ExpressionValue(правки выражений); - операции по работе с
eventHandlers(привязка действий к событиям, генерация обработчиков на основе текста).
- более точные операции для
-
Поддержка function calling / tools
- формальное описание операций как tools в промпте;
- разбор
toolCallsизAiModelResponseи маппинг их вAiOperation[]; - более строгий контракт между моделью и библиотекой.
-
Более строгая валидация ответов
- схемы (JSON Schema / Zod) для проверки структур
AiOperation; - отчёт о пропущенных/исправленных операциях для devtools.
- схемы (JSON Schema / Zod) для проверки структур
-
Расширенное логгирование и трейсинг
- requestId/traceId во всех вызовах;
- хуки для логгирования промпта, ответа и результата применения операций;
- интеграция с общей системой логов платформы.
-
Настройка промпта и профили
- разные системные промпты для режимов: generate / explain / refactor / fix;
- локализация подсказок под язык пользователя;
- возможность конфигурировать стиль ответов (подробность, формальность).
12. Резюме
@lowcode/ai-orchestrator — это единый исполнительный слой для AI‑ассистента, который:
- принимает на вход структурированный запрос
AiAssistRequest(prompt + DSL + контекст); - формирует OpenAI‑подобный
AiModelRequestдля избранного провайдера; - разбирает
AiModelResponseв список операцийAiOperation[]и текстовое объяснение; - аккуратно применяет операции к
AppSchemaи возвращает новый DSL.
Благодаря этому:
- логика работы с AI сосредоточена в одном месте и не дублируется между builder-web и API;
- подключение новых провайдеров (OpenAI, AI Tunnel, локальные модели) сводится к реализации
callModel; - развитие AI‑функций (генерация компонентов, рефакторинг, автоконфиг источников данных и т.п.) происходит без изменения контрактов DSL и без ломки редактора.