Салют, %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, но потерпел неудачу и отказался от этой затеи. Для старта накидал простейшую форму:

Всего четыре элемента:
MemoInput — поле ввода.
ButtonSend — кнопка отправки.
Label1 — сообщение Enter your message.
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, чтобы приложение корректно обрабатывало русский язык. Итого четыре функции:
SendToOllama — отправляет HTTP POST-запрос к локальному серверу Ollama и получает ответ в формате JSON.
UTF8ToAnsiSafe — преобразует строку из UTF-8 в кодировку Windows-1251 (ANSI).
ExtractContent — извлекает подстроку "content":" из JSON-ответа.
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.
Запускаю — и оно работает:

Разумеется, тут есть миллион проблем. Например, ExtractContent пока что не учитывает вложенные кавычки и не использует полноценный JSON-парсер. Некоторые ответы из-за этого могут отображаться неправильно. В поле вывода не хватает прокрутки, а приложение крашится, если текст в поле ввода перенести с помощью Enter. Но все это можно доделать и отполировать позже. Главное, что идея нашла воплощение.
Что в итоге
Наверное, многие из вас скажут: «Ну какой к черту Python или Delphi?». Бери себе C# в паре с WinForms и пиши клиент, который будет максимально близок к возможностям Windows Vista. Наверное, это правильно, но только если знаешь C#. Уверен, что многие читатели без проблем смогут это сделать.
Я же попробовал написать простое приложение под старую операционную систему с минимальными усилиями — и это получилось. Теперь мой ThinkPad будет прекрасно работать отдельным терминалом связи с разными нейронными сетями, запущенными с помощью Ollama. В будущем я планирую доделать клиент на Python, добавив туда удобный интерфейс и реализовав хранение диалогов.
А вам приходилось писать софт под устаревшие операционные системы? Ждем вас в комментариях!