Как стать автором
Обновить
2375.15
МТС
Про жизнь и развитие в IT

Вторая жизнь для ретроноутбука. Пишем клиент Ollama на Python + Tkinter и Delphi 7 для Windows Vista

Время на прочтение8 мин
Количество просмотров4.5K

Салют, %USERNAME%. Признаюсь, я очень люблю старые лэптопы ThinkPad. В те времена, когда брендом владела IBM, эти аппараты восхищали своей продуманностью и функциональностью. Цена на них кусалась, но ты точно знал, что за строгим дизайном скрывается мощное «железо» и отличные инженерные решения. Чего стоила подсветка клавиатуры ThinkLight (познакомился с ней на R61i), которая позволяла с комфортом работать в поезде или самолете, не напрягая других пассажиров включением света. Ну а трекпойнт мне до сих пор нравится больше, чем любой крутой тачпад.

Развитие операционных систем и технологий оставило старые ThinkPad за бортом. Разумный предел для моего X41 Tablet — Windows Vista. Под нее есть все драйверы устройств, и она способна запускать большинство игр и приложений, написанных для Windows XP. Но в современных условиях, когда главным инструментом пользователя стал веб-браузер, эта система безнадежно устарела.

И тут ко мне пришла безумная идея: а что, если подарить такому ноутбуку вторую жизнь и дать возможность работать с современными нейросетями? Разумеется, ресурсов на полноценный инференс не хватит, но вот написать простой клиент для взаимодействия с Ollama — почему бы и нет. В итоге я получу ноутбук, который позволит мне общаться с нейронными сетями и вновь подарит удовольствие от использования. Что получилось из этой затеи, как раз и расскажу дальше.

Разработка для старой операционной системы — это всегда ограничения. Они будут поджидать на каждом шагу: от невозможности поставить актуальные версии привычных библиотек до проблем с отсутствием протоколов вроде TLS 1.2 в Windows Vista. Придется это учитывать на этапе планирования.

Сам по себе обмен данными с сервером Ollama не сложен. Последний предоставляет локальный HTTP API, доступный по адресу: http://[server_ip]:11434/api/generate. На него нужно отправить POST-запрос с промптом, а сервер вернет результат в формате JSON. Его потребуется распарсить и отобразить ответ пользователю.

HTTPS нет, так что одной головной болью меньше. Сервер Ollama у меня запущен в локальной сети, так что можно воспользоваться самым обычным HTTP-протоколом. Получается, что приложение должно уметь отправлять HTTP-запросы и работать с JSON. Теперь стоит подумать о том, какие проблемы возникнут.

Хокку от нейросети
Хокку от нейросети

Python + Tkinter

Казалось бы, самый очевидный вариант. Запросы слать через requests (или urllib), JSON разбирать через стандартную библиотеку, интерфейс рисовать с помощью Tkinter, а exe-шник собрать через cx_Freeze. Но тут возникает сразу масса вопросов:

  • Какая версия Python последняя для Vista?

  • Будет ли функционировать пакетный менеджер pip?

  • Какие версии библиотек нормально заработают?

В процессе беглого гугления выяснилось, что для Windows Vista последней версией Python стала 3.4.4, выпущенная в декабре 2015. Неплохо, но вот pip устарел и уже не сможет скачать пакеты онлайн. Придется искать отдельно и устанавливать вручную. Даже если забить на requests и все сделать через urllib, то как минимум cx_Freeze нужно будет поставить.

Мне удалось найти исходники версии 5.0.2, которые предстояло собрать вручную. Это не так сложно, но вначале потребуется установить Microsoft Visual C++ 2010 Express, без которых эта штука банально на соберется. Официально скачать уже нельзя — на сайте Microsoft висит заглушка. Так что доставать пришлось окольными путями через Internet Archive.

Установив Microsoft Visual C++ 2010 Express, отправил компьютер на перезагрузку и затем собрал cx_Freeze 5.0.2:

python setup.py build

После чего установил в систему:

python setup.py install

Теперь по-быстрому набросал код (app.py):

import tkinter as tk
import urllib.request
import json

OLLAMA_API_URL = "http://[server_ip]:11434/api/generate"

def send_prompt():
    prompt = prompt_entry.get("1.0", tk.END).strip()
    if not prompt:
        return

    response_text.set("Ожидание ответа...")

    try:
        data = json.dumps({
            "model": "deepseek-r1:14b",
            "prompt": prompt,
            "stream": False
        }).encode("utf-8")

        req = urllib.request.Request(
            OLLAMA_API_URL,
            data=data,
            headers={"Content-Type": "application/json"}
        )

        with urllib.request.urlopen(req) as response:
            result = response.read().decode("utf-8")
            result_json = json.loads(result)
            content = result_json.get("response", "Нет текста в ответе.")
            response_text.set(content)

    except Exception as e:
        response_text.set("Ошибка: " + str(e))

root = tk.Tk()
root.title("Ollama Клиент (Vista)")

tk.Label(root, text="Введите запрос:").pack(pady=5)
prompt_entry = tk.Text(root, height=5, width=50)
prompt_entry.pack(pady=5)

tk.Button(root, text="Отправить", command=send_prompt).pack(pady=5)

response_text = tk.StringVar()
tk.Label(root, textvariable=response_text, wraplength=400, justify="left").pack(pady=10)

root.mainloop()

Запустил и попытался поздороваться:

Это ни что иное, как смайлик. Tkinter, встроенный в Python 3.4.4, полноценно Unicode не поддерживает, позволяя отображать только символы с кодами не выше U+FFFF. Значит, придется такие «опасные» фрагменты вырезать полностью из ответа непосредственно перед передачей в StringVar. Меняю строку:

response_text.set(content)

на следующие ('' — это две одинарные кавычки):

safe_content = ''.join(c for c in content if ord(c) <= 0xFFFF)
response_text.set(safe_content)

И пробую еще раз:

Заработало
Заработало

В целом, уже победа. Запросы корректно формируются, приходят правильные ответы. Понятное дело, что нужно добавить очистку поля запроса, отделить рассуждения, обрамленные тегами <think></think>, и сделать приложение более красивым.

Поскольку сейчас запрос отправляется не асинхронно, программа перестает отвечать, пока не получит JSON от сервера. Но даже так концепция работает и имеет право на жизнь. Готовое приложение можно будет легко собрать в exe-шник с помощью такого кода:

from cx_Freeze import setup, Executable

setup(
    name="OllamaVistaClient",
    version="0.1",
    description="Простой GUI-клиент для Ollama",
    executables=[Executable("app.py")],
)

Кладу его рядом, называю setup.py и собираю:

python setup.py build

Вот такая красота получилась:

Файлы готового приложения
Файлы готового приложения

ЗЫ. Только потом заметил, что в системе был указан неправильный год: 2026 вместо 2025. Получилось путешествие в будущее

Delphi 7

Если с Python все относительно просто, то Delphi 7 преподнесла множество сюрпризов. Я одновременно люблю и ненавижу эту среду разработки. С одной стороны, она прекрасно подходит для написания простых утилит. Но с другой — более строга к коду и порой нужно поломать голову, выискивая причину проблемы. Тем не менее в колледже я неплохо ее знал, и поэтому решил вспомнить молодость.

Несмотря на то, что Delphi 7 официально никогда Vista не поддерживала, она прекрасно под ней работает. Некоторые рекомендуют отключить контроль учетных записей (UAC), но у меня с этим сложностей не возникло.

Отсылать запросы буду через компонент InDy (сокращение от Internet Direct) — IdHTTP. Парсинг JSON вначале попробовал сделать через SuperObject, но потерпел неудачу и отказался от этой затеи. Для старта накидал простейшую форму:

Всего четыре элемента:

  1. MemoInput — поле ввода.

  2. ButtonSend — кнопка отправки.

  3. Label1 — сообщение Enter your message.

  4. MemoOutput — поле вывода.

С помощью curl внимательно посмотрел на то, какой ответ мне предстоит распарсить:

{"model":"deepseek-r1:14b","created_at":"2025-04-07T20:31:19.179407862Z","message":{"role":"assistant","content":"\u003cthink\u003e\n\n\u003c/think\u003e\n\nПривет! Как я могу помочь тебе сегодня?"},"done_reason":"stop","done":true,"total_duration":2226372627,"load_duration":18673060,"prompt_eval_count":6,"prompt_eval_duration":169000000,"eval_count":18,"eval_duration":203700

Сразу стало ясно, что придется обрабатывать Escape-последовательности вроде \u003, \u003e и \n. А еще нужно реализовать перекодирование из UTF-8 в ANSI, чтобы приложение корректно обрабатывало русский язык. Итого четыре функции:

  1. SendToOllama — отправляет HTTP POST-запрос к локальному серверу Ollama и получает ответ в формате JSON.

  2. UTF8ToAnsiSafe — преобразует строку из UTF-8 в кодировку Windows-1251 (ANSI).

  3. ExtractContent — извлекает подстроку "content":" из JSON-ответа.

  4. DecodeEscapes — обрабатывает Escape-последовательности.

Финальный вид кода выглядит так:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, IdHTTP;

type
  TForm1 = class(TForm)
    MemoInput: TMemo;
    ButtonSend: TButton;
    MemoOutput: TMemo;
    Label1: TLabel;
    procedure ButtonSendClick(Sender: TObject);
  private
    function SendToOllama(const UserMessage: string): string;
    function UTF8ToAnsiSafe(const UTF8Str: string): string;
    function ExtractContent(const JSONText: string): string;
    function DecodeEscapes(const S: string): string;
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function TForm1.UTF8ToAnsiSafe(const UTF8Str: string): string;
begin
  Result := UTF8Decode(UTF8Str); // Convert UTF-8 to Windows-1251
end;

function TForm1.DecodeEscapes(const S: string): string;
begin
  Result := StringReplace(S, '\n', #13#10, [rfReplaceAll]);
  Result := StringReplace(Result, '\r', '', [rfReplaceAll]);
  Result := StringReplace(Result, '\t', '    ', [rfReplaceAll]);
  Result := StringReplace(Result, '\"', '"', [rfReplaceAll]);
  Result := StringReplace(Result, '\u003c', '<', [rfReplaceAll]);
  Result := StringReplace(Result, '\u003e', '>', [rfReplaceAll]);
  Result := StringReplace(Result, '\u003C', '<', [rfReplaceAll]);
  Result := StringReplace(Result, '\u003E', '>', [rfReplaceAll]);
end;

function TForm1.ExtractContent(const JSONText: string): string;
var
  StartPos, EndPos: Integer;
  Extracted: string;
begin
  Result := '(no content found)';
  StartPos := Pos('"content":"', JSONText);
  if StartPos > 0 then
  begin
    StartPos := StartPos + Length('"content":"');
    EndPos := StartPos;
    while (EndPos <= Length(JSONText)) and not (JSONText[EndPos] in ['"']) do
      Inc(EndPos);
    Extracted := Copy(JSONText, StartPos, EndPos - StartPos);
    Result := DecodeEscapes(Extracted);
  end;
end;

function TForm1.SendToOllama(const UserMessage: string): string;
var
  HTTP: TIdHTTP;
  RequestBody: TStringStream;
  Mem: TMemoryStream;
  JSONStr: UTF8String;
  RawBytes, RawResponse: string;
begin
  HTTP := TIdHTTP.Create(nil);
  RequestBody := TStringStream.Create('');
  Mem := TMemoryStream.Create;
  try
    HTTP.Request.ContentType := 'application/json';
    HTTP.Request.Connection := 'close';

    JSONStr := UTF8Encode(
      '{' +
        '"model":"deepseek-r1:14b",' +
        '"messages":[{"role":"user","content":"' + UserMessage + '"}],' +
        '"stream":false' +
      '}'
    );

    RequestBody.WriteString(string(JSONStr));
    RequestBody.Position := 0;

    HTTP.Post('http://[server_ip]:11434/api/chat', RequestBody, Mem);

    SetLength(RawBytes, Mem.Size);
    Mem.Position := 0;
    Mem.Read(RawBytes[1], Mem.Size);

    RawResponse := UTF8ToAnsiSafe(RawBytes);
    Result := ExtractContent(RawResponse);
  finally
    HTTP.Free;
    RequestBody.Free;
    Mem.Free;
  end;
end;

procedure TForm1.ButtonSendClick(Sender: TObject);
begin
  MemoOutput.Lines.Text := SendToOllama(MemoInput.Lines.Text);
end;

end.

Запускаю — и оно работает:

Deepseek-R1 хокку пишет не очень, а вот Llama 3.1 вполне годно
Deepseek-R1 хокку пишет не очень, а вот Llama 3.1 вполне годно

Разумеется, тут есть миллион проблем. Например, ExtractContent пока что не учитывает вложенные кавычки и не использует полноценный JSON-парсер. Некоторые ответы из-за этого могут отображаться неправильно. В поле вывода не хватает прокрутки, а приложение крашится, если текст в поле ввода перенести с помощью Enter. Но все это можно доделать и отполировать позже. Главное, что идея нашла воплощение.

Что в итоге

Наверное, многие из вас скажут: «Ну какой к черту Python или Delphi?». Бери себе C# в паре с WinForms и пиши клиент, который будет максимально близок к возможностям Windows Vista. Наверное, это правильно, но только если знаешь C#. Уверен, что многие читатели без проблем смогут это сделать.

Я же попробовал написать простое приложение под старую операционную систему с минимальными усилиями — и это получилось. Теперь мой ThinkPad будет прекрасно работать отдельным терминалом связи с разными нейронными сетями, запущенными с помощью Ollama. В будущем я планирую доделать клиент на Python, добавив туда удобный интерфейс и реализовав хранение диалогов.

А вам приходилось писать софт под устаревшие операционные системы? Ждем вас в комментариях!

Теги:
Хабы:
+47
Комментарии18

Полезные ссылки

Обходим подводные камни работы с UDA в коде на Lua для ScyllaDB: дружим Java-драйвер и пустые значения

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров333
Всего голосов 5: ↑5 и ↓0+10
Комментарии0

Пайплайн распознавания номеров транспортных средств: как это устроено

Время на прочтение7 мин
Количество просмотров2.1K
Всего голосов 23: ↑22 и ↓1+25
Комментарии1

Интеграция виджета обратного звонка МТС Exolve в документацию на MkDocs

Время на прочтение8 мин
Количество просмотров398
Всего голосов 5: ↑5 и ↓0+7
Комментарии0

Путь видео в онлайн-кинотеатрах от «стекла до стекла». Middleware — ядро, подписки, сервисы, витрина

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров729
Всего голосов 4: ↑3 и ↓1+4
Комментарии0

Приручая хаос: как структурировать процессы в эксплуатационных командах. Кейс МТС

Время на прочтение6 мин
Количество просмотров684
Всего голосов 3: ↑3 и ↓0+4
Комментарии0

Информация

Сайт
www.mts.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия