Как известно, под новый год случаются чудеса, и этот год не стал исключением. Мне удалось прикрутить LLM в визуальный язык программирования Scratch, чем и обрадКак известно, под новый год случаются чудеса, и этот год не стал исключением. Мне удалось прикрутить LLM в визуальный язык программирования Scratch, чем и обрад

Новогодний подарок: Как я прикрутил LLM к scratch и порадовал ребёнка

Как известно, под новый год случаются чудеса, и этот год не стал исключением. Мне удалось прикрутить LLM в визуальный язык программирования Scratch, чем и обрадовал ребенка. А началось всё в один прекрасный день, когда мой сын - школьник осваивал n8n и ваял телеграм бота. Разговорившись, мы вспомнили, что его увлечение программированием началось со Scratch. И его фраза, что было бы здорово, если бы в scratch была бы встроена иишечка, можно столько прикольных игр сделать, стала отправной точкой для данного проекта. Рассказываю и показываю, как мы реализовали эту безумную идею.

Типичная нейротян из Scratch
Типичная нейротян из Scratch

После апгрейда компьютера Scratch не был установлен в системе. Поэтому первым делом я скачал его и... почти сразу же удалил. Дело в том что он не умеет в HTTP-запросы. А без них общение с внешними API невозможны. Казалось бы можно сворачивать проект и не страдать больше фигнёй, но тут на сцену вышел Turbowarp. По сути, это тот же самый Scratch, только на максималках. Его ключевая фишка - поддержка пользовательских расширений (extension). То есть мы можем написать собственный extensions и добавить недостающую функциональность.

Интерфейс Turbowarp. Найдите 10 отличий со Scratch
Интерфейс Turbowarp. Найдите 10 отличий со Scratch

В качестве мозга был выбран Gigachat. Во-первых он бесплатный. Во-вторых не надо заморачиваться с VPN, а в третьих он работает без vpn и бесплатен.

Для подключения к API GigaChat необходимо:
  1. Зарегистрироваться в Studio по ссылке https://developers.sber.ru/studio/workspaces/

  2. Создать проект, в инструментах выбрать GigaChat API и заполнить все необходимые поля.

  3. Зайти в созданный проект и выбрать: "Настроить API"

  4. Получить и сохранить ключ.

Подготовительная часть окончена, теперь создадим прокси сервер. Он необходим из-за политики CORS, которая блокирует запросы к внешним API (позже я узнал, что в desktop версии можно отключить CORS, но было уже поздно. Для web версии сервер все равно необходим). Сервер будет:

  1. Принимать запросы от Turbowarp.

  2. Перенаправлять их в Gigachat.

  3. Возвращать ответ обратно в Scratch-проект.

Так как проект чисто для себя (и сына) то сервер будет локальный. Мой выбор в качестве фреймворка пал на FastAPI.

app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

Как отмечал выше: проект локальный, поэтому разрешаем CORS со всех источников без лишних угрызений совести.

Настраиваем GIGACHAT.

GIGACHAT_AUTH_URL = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth" GIGACHAT_CHAT_URL = "https://gigachat.devices.sberbank.ru/api/v1/chat/completions" AUTH_KEY = os.getenv("GIGACHAT_AUTH_KEY") SCOPE = "GIGACHAT_API_PERS" MODEL_NAME = "GigaChat" access_token = None token_expires_at = 0

Так как токен живет 30 минут реализуем функцию, которая будет возвращать новый токен, после истечении отведенного времени

async def ensure_token(): global access_token, token_expires_at now = time.time() if access_token and now < token_expires_at - 60: return access_token headers = { "Authorization": f"Basic {AUTH_KEY}", "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "RqUID": str(uuid.uuid4()), } data = f"scope={SCOPE}" async with httpx.AsyncClient(verify=False, timeout=30) as client: response = await client.post( GIGACHAT_AUTH_URL, headers=headers, content=data ) response.raise_for_status() token_data = response.json() access_token = token_data["access_token"] token_expires_at = now + token_data.get("expires_in", 1800) return access_token

API будет максимально простым и без всяких наворотов. Реализованы всего три метода:

  • Отправляем сообщение

  • Получаем ответ

  • Очищаем чат

Полный код сервера

import time import uuid from fastapi import FastAPI, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import httpx import os # ===================== # APP # ===================== app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ===================== # STATE # ===================== chat_history = [] last_answer = "" state = "idle" # idle | processing | ready # ===================== # GIGACHAT CONFIG # ===================== GIGACHAT_AUTH_URL = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth" GIGACHAT_CHAT_URL = "https://gigachat.devices.sberbank.ru/api/v1/chat/completions" AUTH_KEY = os.getenv("GIGACHAT_AUTH_KEY") SCOPE = "GIGACHAT_API_PERS" MODEL_NAME = "GigaChat" access_token = None token_expires_at = 0 # ===================== # MODELS # ===================== class SendRequest(BaseModel): message: str # ===================== # TOKEN # ===================== async def ensure_token(): global access_token, token_expires_at now = time.time() if access_token and now < token_expires_at - 60: return access_token headers = { "Authorization": f"Basic {AUTH_KEY}", "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "RqUID": str(uuid.uuid4()), } data = f"scope={SCOPE}" async with httpx.AsyncClient(verify=False, timeout=30) as client: response = await client.post( GIGACHAT_AUTH_URL, headers=headers, content=data ) response.raise_for_status() token_data = response.json() access_token = token_data["access_token"] token_expires_at = now + token_data.get("expires_in", 1800) return access_token # ===================== # GIGACHAT REQUEST # ===================== async def request_gigachat(): global chat_history, last_answer, state try: token = await ensure_token() headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } payload = { "model": MODEL_NAME, "messages": chat_history } async with httpx.AsyncClient(verify=False, timeout=60) as client: response = await client.post( GIGACHAT_CHAT_URL, headers=headers, json=payload ) response.raise_for_status() data = response.json() answer = data["choices"][0]["message"]["content"] chat_history.append({ "role": "assistant", "content": answer }) last_answer = answer state = "ready" except Exception as e: print("error: ", repr(e)) last_answer = "" state = "idle" # ===================== # API # ===================== @app.post("/clear") async def clear_chat(): global chat_history, last_answer, state chat_history = [] last_answer = "" state = "idle" print("Chat cleared") return {"status": "cleared"} @app.post("/send") async def send_message(req: SendRequest, background_tasks: BackgroundTasks): global chat_history, last_answer, state if state == "processing": return {"status": "busy"} chat_history.append({ "role": "user", "content": req.message }) last_answer = "" state = "processing" print("Message received, querying GigaChat") background_tasks.add_task(request_gigachat) return {"status": "accepted"} @app.get("/get") async def get_answer(): return { "answer": last_answer if state == "ready" else "" }

Запускаем сервер через терминал

uvicorn server:api --host 127.0.0.1 --port 8000

Или в режиме разработки, для автоматической перезагрузки при изменении кода:

uvicorn server:app --host 127.0.0.1 --port 8000 --reload

Когда сервер написан и запущен, переходим к заключительному этапу: пишем расширение для turbowarp. Итак нам нужно всего четыре элемента:

  1. Подключиться к серверу

  2. Отправить запрос

  3. Принять ответ

  4. Очистить чат

Код расширения

(function (Scratch) { "use strict"; let serverUrl = ""; let lastAnswer = ""; class GigaChatProxy { getInfo() { return { id: "gigachatproxy", name: "GigaChat Proxy", blocks: [ { opcode: "connect", blockType: Scratch.BlockType.COMMAND, text: "подключиться к серверу [URL]", arguments: { URL: { type: Scratch.ArgumentType.STRING, defaultValue: "http://127.0.0.1:8000" } } }, { opcode: "clearChat", blockType: Scratch.BlockType.COMMAND, text: "очистить чат" }, { opcode: "sendMessage", blockType: Scratch.BlockType.COMMAND, text: "отправить в чат [TEXT]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, defaultValue: "Привет" } } }, { opcode: "getAnswer", blockType: Scratch.BlockType.REPORTER, text: "получить ответ" } ] }; } connect({ URL }) { serverUrl = URL; lastAnswer = ""; } async clearChat() { if (!serverUrl) return; try { await fetch(serverUrl + "/clear", { method: "POST" }); } catch (e) { // ошибки игнорируем } lastAnswer = ""; } async sendMessage({ TEXT }) { if (!serverUrl) return; lastAnswer = ""; try { await fetch(serverUrl + "/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: TEXT }) }); } catch (e) { // ошибки игнорируем } } async getAnswer() { if (!serverUrl) return ""; try { const response = await fetch(serverUrl + "/get"); const data = await response.json(); if (data.answer && data.answer !== "") { lastAnswer = data.answer; } } catch (e) { // игнор } return lastAnswer; } } Scratch.extensions.register(new GigaChatProxy()); })(Scratch);

Теперь можно в бой!
Теперь можно в бой!

После подключения расширения появляются новые блоки, которые можно использовать в проектах.

По традиции пишем "Hello, world!". Меняем спрайт на радующее глаз изображение и общаемся с моделью.

Hello, World!
И что ты мне на это ответишь?
И что ты мне на это ответишь?
Ты что такая дерзкая, а?
Ты что такая дерзкая, а?

С чувством глубокого удовлетворения я отправился заниматься своими делами (спать), а юного скретчера попросил подумать над играми, которые он так мечтал реализовать. Он за словом в карман не полез и назначил ценник в 1000 рублей за игру.

Камень, ножницы, бумага
Камень, ножницы, бумага

На следующий день я увидел игру "Камень, ножницы, бумага". По сути, это стрельба из пушки по воробьям. Для такой игры можно вообще обойтись без LLM. Единственный плюс: победитель объявляется каждый раз по разному.

Разработчик пояснил, что это всего лишь пристрелка.

Следующим он показал проект, который пока еще в разработке. Мы играем за детектива и, опрашивая подозреваемых, должны вычислить преступника.

Убийца - садовник
Что вы делали вечером, накануне, преступления?
Что вы делали вечером, накануне, преступления?
Минздрав предупреждает!!!
Минздрав предупреждает!!!

Вот здесь уже стало действительно интересно. Но сразу всплыли ограничения: В облачко ответа персонажа действует лимит 300 символов. Завязка истории представляет длинный текст и возникает такая же проблема с его выводом. Если вывести на экран переменную с этим текстом, то она все перекроет. В ней нет скроллинга и если текст не влез, то ты его просто не увидишь. В итоге пришлось делить текст на части и выводить их поэтапно.

В целом это был интересный эксперимент, пусть и с костылями, но затащили в scratch LLM. И работает не так гладко, как хотелось, но сын доволен, а это главный критерий успеха.

Источник

Возможности рынка
Логотип Large Language Model
Large Language Model Курс (LLM)
$0.0003345
$0.0003345$0.0003345
-4.01%
USD
График цены Large Language Model (LLM) в реальном времени
Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.