Салют, %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, добавив туда удобный интерфейс и реализовав хранение диалогов.
А вам приходилось писать софт под устаревшие операционные системы? Ждем вас в комментариях!