Реализация AI Copilot для пользователей веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация AI Copilot для пользователей веб-приложения
Сложная
~2-4 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация AI Copilot для пользователей веб-приложения

AI Copilot — это встроенный в интерфейс приложения интеллектуальный помощник, который работает в контексте текущего экрана, данных пользователя и его истории действий. Это не чат-бот в углу страницы и не всплывающее меню с подсказками — это компонент, который понимает, что пользователь делает прямо сейчас, и предлагает конкретные следующие шаги.

Разница между «добавить ChatGPT на сайт» и «реализовать AI Copilot» принципиальная. Первое — это обёртка над API с полем ввода. Второе требует проектирования контекстного протокола, архитектуры инструментов (tool calls), управления историей сессии и интеграции с доменной логикой приложения.

Архитектура: что внутри Copilot

Copilot состоит из нескольких слоёв:

Context layer — сбор и форматирование контекста перед каждым запросом к модели. Включает текущий маршрут, состояние UI, релевантные данные пользователя, последние действия.

Tool layer — набор функций, которые модель может вызывать: поиск по данным, выполнение операций в приложении, получение справочной информации.

Conversation layer — управление историей диалога, сжатие длинных сессий, разграничение контекстов между разными задачами.

UI layer — виджет, который интегрируется в существующий интерфейс без разрушения UX.

Реализация: стек и подход

Бэкенд строится на Node.js или Python. Модель — GPT-4o или Claude 3.5 Sonnet в зависимости от задачи. Коммуникация через Server-Sent Events для стриминга ответов.

// copilot/context-builder.ts
interface CopilotContext {
  route: string;
  pageData: Record<string, unknown>;
  userProfile: {
    id: string;
    role: string;
    recentActions: Action[];
  };
  selectedEntities?: Entity[];
}

export function buildContext(req: Request): CopilotContext {
  return {
    route: req.path,
    pageData: req.body.pageData ?? {},
    userProfile: {
      id: req.user.id,
      role: req.user.role,
      recentActions: getRecentActions(req.user.id, 10),
    },
    selectedEntities: req.body.selectedEntities,
  };
}

Tool calls — ключевая часть. Модель не просто отвечает текстом, она вызывает функции приложения:

// copilot/tools.ts
const tools: ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "search_records",
      description: "Search records in the current module by query",
      parameters: {
        type: "object",
        properties: {
          query: { type: "string" },
          module: { type: "string", enum: ["orders", "customers", "products"] },
          limit: { type: "number", default: 10 },
        },
        required: ["query", "module"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "create_record",
      description: "Create a new record in the specified module",
      parameters: {
        type: "object",
        properties: {
          module: { type: "string" },
          data: { type: "object" },
        },
        required: ["module", "data"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "get_analytics",
      description: "Get aggregated analytics for a date range",
      parameters: {
        type: "object",
        properties: {
          metric: { type: "string" },
          from: { type: "string", format: "date" },
          to: { type: "string", format: "date" },
        },
        required: ["metric", "from", "to"],
      },
    },
  },
];

Обработчик запроса с поддержкой многошаговых вызовов инструментов:

// copilot/handler.ts
export async function handleCopilotRequest(
  messages: Message[],
  context: CopilotContext,
  res: Response
) {
  const systemPrompt = buildSystemPrompt(context);

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");

  let currentMessages = [
    { role: "system", content: systemPrompt },
    ...messages,
  ];

  // Цикл для multi-step tool calls
  while (true) {
    const stream = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: currentMessages,
      tools,
      stream: true,
    });

    let toolCallAccumulator: ToolCall[] = [];
    let textBuffer = "";

    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta;

      if (delta?.content) {
        textBuffer += delta.content;
        res.write(`data: ${JSON.stringify({ type: "text", content: delta.content })}\n\n`);
      }

      if (delta?.tool_calls) {
        // Аккумулируем tool calls из стрима
        mergeToolCallChunks(toolCallAccumulator, delta.tool_calls);
      }

      if (chunk.choices[0]?.finish_reason === "tool_calls") {
        // Выполняем все запрошенные инструменты
        const toolResults = await executeToolCalls(toolCallAccumulator, context);

        currentMessages = [
          ...currentMessages,
          { role: "assistant", tool_calls: toolCallAccumulator },
          ...toolResults.map((r) => ({
            role: "tool" as const,
            tool_call_id: r.id,
            content: JSON.stringify(r.result),
          })),
        ];

        // Сообщаем фронтенду о выполненных действиях
        res.write(`data: ${JSON.stringify({ type: "tool_result", results: toolResults })}\n\n`);
        break; // Продолжаем цикл
      }

      if (chunk.choices[0]?.finish_reason === "stop") {
        res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
        res.end();
        return;
      }
    }
  }
}

Системный промпт: контекстная привязка

Промпт не статический. Он собирается под каждый запрос и включает состояние приложения:

function buildSystemPrompt(context: CopilotContext): string {
  return `
You are a Copilot assistant embedded in a business application. You help users work with their data efficiently.

Current context:
- Page: ${context.route}
- User role: ${context.userProfile.role}
- Recent actions: ${context.userProfile.recentActions.map(a => a.description).join(", ")}
${context.selectedEntities?.length ? `- Selected items: ${JSON.stringify(context.selectedEntities)}` : ""}

Page data summary:
${JSON.stringify(context.pageData, null, 2).slice(0, 2000)}

Rules:
- Be concise and action-oriented
- When you can take action via tools, prefer doing so over just describing how
- Confirm before destructive operations
- If you need more context, ask one targeted question
- Respond in the language the user writes in
`.trim();
}

Фронтенд: виджет Copilot

React-компонент, который живёт поверх основного интерфейса:

// components/Copilot/CopilotPanel.tsx
import { useState, useRef, useEffect } from "react";
import { useCopilotContext } from "./CopilotContext";

export function CopilotPanel() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const context = useCopilotContext();
  const abortRef = useRef<AbortController | null>(null);

  async function sendMessage() {
    if (!input.trim() || isStreaming) return;

    const userMessage = { role: "user" as const, content: input };
    const newMessages = [...messages, userMessage];
    setMessages(newMessages);
    setInput("");
    setIsStreaming(true);

    abortRef.current = new AbortController();

    const response = await fetch("/api/copilot", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        messages: newMessages,
        pageData: context.pageData,
        selectedEntities: context.selectedEntities,
      }),
      signal: abortRef.current.signal,
    });

    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let assistantContent = "";

    // Добавляем пустое сообщение ассистента для стриминга
    setMessages((prev) => [...prev, { role: "assistant", content: "" }]);

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const lines = decoder.decode(value).split("\n");
      for (const line of lines) {
        if (!line.startsWith("data: ")) continue;
        const event = JSON.parse(line.slice(6));

        if (event.type === "text") {
          assistantContent += event.content;
          setMessages((prev) => {
            const updated = [...prev];
            updated[updated.length - 1] = {
              role: "assistant",
              content: assistantContent,
            };
            return updated;
          });
        }

        if (event.type === "tool_result") {
          // Показываем, какие действия были выполнены
          context.onToolsExecuted?.(event.results);
        }
      }
    }

    setIsStreaming(false);
  }

  return (
    <aside className="copilot-panel">
      <div className="copilot-messages">
        {messages.map((msg, i) => (
          <MessageBubble key={i} message={msg} />
        ))}
      </div>
      <div className="copilot-input">
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()}
          placeholder="Спросите что-нибудь или попросите выполнить действие..."
        />
        <button onClick={sendMessage} disabled={isStreaming}>
          {isStreaming ? "Остановить" : "Отправить"}
        </button>
      </div>
    </aside>
  );
}

Контекстный провайдер отслеживает состояние страницы и передаёт его в Copilot:

// components/Copilot/CopilotContext.tsx
export function CopilotProvider({ children }: { children: React.ReactNode }) {
  const location = useLocation();
  const [pageData, setPageData] = useState({});
  const [selectedEntities, setSelectedEntities] = useState<Entity[]>([]);

  // Страницы регистрируют свой контекст через хук
  const registerContext = useCallback((data: Record<string, unknown>) => {
    setPageData(data);
  }, []);

  return (
    <CopilotContext.Provider
      value={{ pageData, selectedEntities, setSelectedEntities, registerContext }}
    >
      {children}
    </CopilotContext.Provider>
  );
}

// Хук для страниц
export function useCopilotRegister(data: Record<string, unknown>) {
  const { registerContext } = useCopilotContext();
  useEffect(() => {
    registerContext(data);
  }, [JSON.stringify(data)]);
}

Управление историей сессии

Длинные диалоги упираются в лимит контекстного окна. Решение — скользящее окно с суммаризацией:

async function compressHistory(messages: Message[]): Promise<Message[]> {
  if (messages.length <= 20) return messages;

  // Оставляем последние 10 сообщений как есть
  const recent = messages.slice(-10);
  const older = messages.slice(0, -10);

  // Суммаризируем старую часть
  const summary = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "user",
        content: `Summarize this conversation history concisely, preserving key facts and decisions:\n\n${older.map((m) => `${m.role}: ${m.content}`).join("\n")}`,
      },
    ],
  });

  return [
    {
      role: "system",
      content: `Previous conversation summary: ${summary.choices[0].message.content}`,
    },
    ...recent,
  ];
}

Авторизация инструментов

Инструменты должны выполняться с правами пользователя, а не системными:

async function executeToolCalls(
  toolCalls: ToolCall[],
  context: CopilotContext
): Promise<ToolResult[]> {
  return Promise.all(
    toolCalls.map(async (call) => {
      const args = JSON.parse(call.function.arguments);

      // Каждый инструмент проверяет права через тот же middleware что и REST API
      switch (call.function.name) {
        case "search_records":
          return {
            id: call.id,
            result: await searchRecords(args, context.userProfile.id),
          };
        case "create_record":
          // Проверяем право на создание
          await assertPermission(context.userProfile, "create", args.module);
          return {
            id: call.id,
            result: await createRecord(args, context.userProfile.id),
          };
        default:
          return { id: call.id, result: { error: "Unknown tool" } };
      }
    })
  );
}

Что нужно для старта

Перед реализацией нужно определить:

  • Какие операции Copilot должен уметь выполнять (не только читать, но и писать?)
  • Как выглядит контекст на каждой ключевой странице приложения
  • Где хранить историю сессий (Redis для активных, PostgreSQL для архива)
  • Нужна ли мультимодальность (анализ скриншотов, файлов)

Работа разбивается на итерации: сначала read-only Copilot (только отвечает на вопросы по данным), затем добавление инструментов записи по одному модулю. Так можно запустить что-то рабочее за 2–3 недели и расширять дальше.