Net-Base Maġazin

15.07.2026

ChatGPT API ma' Delphi FMX/VCL: Konnessjoni robusta b'Streaming, Retry u parsing nadif tal-JSON

Kif tgħaqqad b’mod robust il-ChatGPT API ma’ Delphi FMX/VCL: HTTP‑client b’kontrolli ta’ timeout u mekanismi ta’ retry, SSE‑streaming mingħajr freeze tal‑UI, u parsjar JSON robust għal sejħiet ta’ għodda u każijiet ta’ żball.

15.07.2026

Minn suġġett tar-rivista għall-prattika tal-proġett

Paġni ta' servizz u paġni tekniċi relevanti għall-artiklu

Għaliex „ChatGPT API mit Delphi FMX/VCL“ fil-prattika mhuwiex biss POST

Min jixtieq jintegra l-ChatGPT API mit Delphi FMX/VCL, malajr jispiċċa fuq sempliċi HTTP-POST. Fil-ambjenti ta’ software tan-negozju reali dan jidher problematiku f’tliet punti: (1) Timeouts u Retries għandhom ikunu deterministiċi, għax inkella l-utenti jiffaċċjaw UI imblukkata, (2) Streaming (Server-Sent Events, qasir SSE) spiss jservi für UX tajba, iżda fil-Delphi-threading jista’ jkun suxxettibbli għal żbalji, u (3) JSON mhux biss “oġġett”: messaġġi ta’ żball, problemi ta’ kwota, kampi vojta jew forom ta’ risposta ftit immodifikati għandhom jiġu mmaniġġjati b’mod robust.

Il-qoxra ta’ sors li ġejja turi approċċ li jaħdem kemm f’FMX kif ukoll f’VCL: client dedikat u testabbli, li jista’ jaħdem kemm non-streaming kif ukoll streaming, li jimmarshalja aġġornamenti tal-UI b’mod nadif (jipprovdi s-sinkronizzazzjoni għall-Main-Thread) u li jżomm logijiet ta’ żball b’informazzjoni utli. Barra minn hekk, huwa mmanifatturat sabiex jinġib fil-ġerarkiji ta’ layer li diġà hemm (pereżempju “API-Client” fis-saff ta’ integrazzjoni, fejn il-UI tibqa’ sempliċi).

Skema tal-arkitettura: iddekoppla l-UI, żomm il-client testabbli

Fi proġetti Delphi b’istorja twila spiss insibu “HTTP im ButtonClick”. Dan jaħdem sakemm jinqala’ l-ewwel inċident. Rakkomandat huwa client żgħir b’dan li ġej:

  • Konfigurazzjoni: Base-URL, API-Key, Modell, Timeouts.
  • Saff tat-trasport: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-Optionen (skont l-operat).
  • Parser: JSON-decoding, oġġetti ta’ żball, estrazzjoni tar-riżultati.
  • UI-Hooks: callback għal Token-/Text-Streaming, iżda mingħajr dipendenza stretta fuq kontrolli VCL/FMX.

B’hekk l-integrazzjoni f’soluzzjonijiet ta’ software korporattiv personalizzati tista’ ssir b’mod ordnat: il-client jista’ jerġa’ jintuża fi servizzi, desktop-clients, għodod ta’ amministrazzjoni jew test-harnesses.

Source-Schnipsel: Delphi-Client mit SSE-Streaming, Timeout/Retry und robustem JSON

Il-kodiċi juża THTTPClient (System.Net.HttpClient) u jipparsa b’mod intenzjonalment minimalistiku b’System.JSON. Għal SSE isir qari linja b’linja u nirreaġixxu għal “data: …”. Dan mhux WebSocket, iżda HTTP-Response-Stream li joħroġ kontinwament linji ta’ test. Importanti: naqraw f’worker-thread u nimmarshaljaw l-aġġornamenti tal-UI fil-Main-Thread.

unit Net-Base.OpenAI.ChatClient;

interface

uses
System.SysUtils, System.Classes, System.Net.URLClient, System.Net.HttpClient,
System.Net.HttpClientComponent, System.JSON, System.Threading,
System.SyncObjs;

type
EChatApiError = class(Exception)
private
FHttpStatus: Integer;
FResponseText: string;
public
constructor Create(const Msg: string; AHttpStatus: Integer; const AResponseText: string);
property HttpStatus: Integer read FHttpStatus;
property ResponseText: string read FResponseText;
end;

TChatStreamEvent = reference to procedure(const AChunkText: string; AIsFinal: Boolean);

TChatCompletionOptions = record
Model: string;
Temperature: Double;
MaxTokens: Integer;
constructor Create(const AModel: string; ATemperature: Double = 0.2; AMaxTokens: Integer = 512);
end;

TOpenAIChatClient = class
private
FBaseUrl: string;
FApiKey: string;
FConnectTimeoutMs: Integer;
FResponseTimeoutMs: Integer;

function BuildChatRequestBody(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
AStream: Boolean): TJSONObject;

function ExtractTextFromNonStreamingResponse(const AJsonText: string): string;
function TryExtractErrorMessage(const AJsonText: string; out AMessage: string): Boolean;

procedure ApplyAuthHeaders(ARequest: IHTTPRequest);

function ExecuteWithRetry(const ADoRequest: TFunc): IHTTPResponse;
public
constructor Create(const ABaseUrl, AApiKey: string);

property ConnectTimeoutMs: Integer read FConnectTimeoutMs write FConnectTimeoutMs;
property ResponseTimeoutMs: Integer read FResponseTimeoutMs write FResponseTimeoutMs;

function ChatOnce(const AUserPrompt: string; const AOptions: TChatCompletionOptions): string;

procedure ChatStream(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
const AOnEvent: TChatStreamEvent);
end;

implementation

{ EChatApiError }

constructor EChatApiError.Create(const Msg: string; AHttpStatus: Integer; const AResponseText: string);
begin
inherited Create(Msg);
FHttpStatus := AHttpStatus;
FResponseText := AResponseText;
end;

{ TChatCompletionOptions }

constructor TChatCompletionOptions.Create(const AModel: string; ATemperature: Double; AMaxTokens: Integer);
begin
Model := AModel;
Temperature := ATemperature;
MaxTokens := AMaxTokens;
end;

{ TOpenAIChatClient }

constructor TOpenAIChatClient.Create(const ABaseUrl, AApiKey: string);
begin
inherited Create;
FBaseUrl := ABaseUrl.TrimRight([‚/‘]);
FApiKey := AApiKey;
FConnectTimeoutMs := 8000;
FResponseTimeoutMs := 60000;
end;

procedure TOpenAIChatClient.ApplyAuthHeaders(ARequest: IHTTPRequest);
begin
// Bearer Token: hawn l-API-Key bħala „Authorization: Bearer …“.
// F’ambjenti korporattivi, kun żgur li l-keys ma jinkitbux fil-log.
ARequest.AddHeader(‚Authorization‘, ‚Bearer ‚ + FApiKey);
ARequest.AddHeader(‚Content-Type‘, ‚application/json‘);
ARequest.AddHeader(‚Accept‘, ‚application/json‘);
end;

function TOpenAIChatClient.BuildChatRequestBody(const AUserPrompt: string;
const AOptions: TChatCompletionOptions; AStream: Boolean): TJSONObject;
var
Msgs: TJSONArray;
Msg: TJSONObject;
begin
Result := TJSONObject.Create;
Result.AddPair(‚model‘, AOptions.Model);
Result.AddPair(‚temperature‘, TJSONNumber.Create(AOptions.Temperature));
Result.AddPair(‚max_tokens‘, TJSONNumber.Create(AOptions.MaxTokens));
Result.AddPair(’stream‘, TJSONBool.Create(AStream));

// Struttura minima tal-‚messages‘ (Chat Completions): role/content.
Msgs := TJSONArray.Create;
Msg := TJSONObject.Create;
Msg.AddPair(‚role‘, ‚user‘);
Msg.AddPair(‚content‘, AUserPrompt);
Msgs.AddElement(Msg);
Result.AddPair(‚messages‘, Msgs);
end;

function TOpenAIChatClient.TryExtractErrorMessage(const AJsonText: string; out AMessage: string): Boolean;
var
J: TJSONValue;
EObj: TJSONObject;
begin
Result := False;
AMessage := “;

J := TJSONObject.ParseJSONValue(AJsonText);
try
if (J is TJSONObject) then
begin
// Formola komuni: { „error“: { „message“: „…“, „type“: „…“ } }
EObj := (J as TJSONObject).GetValue<TJSONObject>(‚error‘);
if Assigned(EObj) then
begin
AMessage := EObj.GetValue<string>(‚message‘, “);
Result := AMessage <> “;
end;
end;
finally
J.Free;
end;
end;

function TOpenAIChatClient.ExtractTextFromNonStreamingResponse(const AJsonText: string): string;
var
J: TJSONValue;
Root: TJSONObject;
Choices: TJSONArray;
Choice0: TJSONObject;
Msg: TJSONObject;
begin
Result := “;

J := TJSONObject.ParseJSONValue(AJsonText);
try
if not (J is TJSONObject) then
raise EChatApiError.Create(‚Tweġiba JSON mhux mistenni (mhux oġġett).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Tweġiba JSON mhux mistenni: choices nieqsa/vojt.‘, 0, AJsonText);

Choice0 := Choices.Items[0] as TJSONObject;
// Chat Completions: choices[0].message.content
Msg := Choice0.GetValue<TJSONObject>(‚message‘);
if Msg = nil then
raise EChatApiError.Create(‚Tweġiba JSON mhux mistenni: message nieqsa.‘, 0, AJsonText);

Result := Msg.GetValue<string>(‚content‘, “);
finally
J.Free;
end;
end;

function TOpenAIChatClient.ExecuteWithRetry(const ADoRequest: TFunc<IHTTPResponse>): IHTTPResponse;
const
MaxAttempts = 3;
var
Attempt: Integer;
DelayMs: Integer;
begin
DelayMs := 350;
for Attempt := 1 to MaxAttempts do
begin
try
Exit(ADoRequest());
except
on E: ENetHTTPClientException do
begin
// Żbalji tan-netwerk/TLS/timeout: retry sempliċi b’backoff.
if Attempt = MaxAttempts then
raise;
Sleep(DelayMs);
DelayMs := DelayMs * 2;
end;
end;
end;
Result := nil;
end;

function TOpenAIChatClient.ChatOnce(const AUserPrompt: string; const AOptions: TChatCompletionOptions): string;
var
Http: THTTPClient;
Req: IHTTPRequest;
Resp: IHTTPResponse;
Body: TJSONObject;
Payload: TStringStream;
RespText: string;
ErrMsg: string;
begin
Http := THTTPClient.Create;
try
Http.ConnectionTimeout := FConnectTimeoutMs;
Http.ResponseTimeout := FResponseTimeoutMs;

Body := BuildChatRequestBody(AUserPrompt, AOptions, False);
try
Payload := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
try
Req := Http.GetRequest(‚POST‘, FBaseUrl + ‚/v1/chat/completions‘);
ApplyAuthHeaders(Req);

Resp := ExecuteWithRetry(
function: IHTTPResponse
begin
Result := Http.Execute(Req, Payload);
end
);

RespText := Resp.ContentAsString(TEncoding.UTF8);

if (Resp.StatusCode < 200) or (Resp.StatusCode >= 300) then
begin
if TryExtractErrorMessage(RespText, ErrMsg) then
raise EChatApiError.Create(ErrMsg, Resp.StatusCode, RespText)
else
raise EChatApiError.Create(‚Żball HTTP ‚ + Resp.StatusCode.ToString, Resp.StatusCode, RespText);
end;

Result := ExtractTextFromNonStreamingResponse(RespText);
finally
Payload.Free;
end;
finally
Body.Free;
end;
finally
Http.Free;
end;
end;

procedure TOpenAIChatClient.ChatStream(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
const AOnEvent: TChatStreamEvent);
var
Task: ITask;
begin
// Ibda streaming b’mod asinkronu b’mod intenzjonat sabiex FMX/VCL ma jibqax imblukkat.
Task := TTask.Run(
procedure
var
Http: THTTPClient;
Req: IHTTPRequest;
Resp: IHTTPResponse;
Body: TJSONObject;
Payload: TStringStream;
Stream: TStream;
Reader: TStreamReader;
Line, Data: string;
J: TJSONValue;
Delta, Choice0, Choices: TJSONValue;
ContentChunk: string;
begin
Http := THTTPClient.Create;
try
Http.ConnectionTimeout := FConnectTimeoutMs;
Http.ResponseTimeout := FResponseTimeoutMs;

Body := BuildChatRequestBody(AUserPrompt, AOptions, True);
try
Payload := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
try
Req := Http.GetRequest(‚POST‘, FBaseUrl + ‚/v1/chat/completions‘);
ApplyAuthHeaders(Req);
Req.AddHeader(‚Accept‘, ‚text/event-stream‘);

Resp := ExecuteWithRetry(
function: IHTTPResponse
begin
Result := Http.Execute(Req, Payload);
end
);

if (Resp.StatusCode < 200) or (Resp.StatusCode >= 300) then
begin
// F’każ ta‘ żbalji fil-streaming, il-kontenut spiss jibqa‘ JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚It-tluq tal-streaming falliet (HTTP ‚ + Resp.StatusCode.ToString + ‚).‘, True);
end);
Exit;
end;

Stream := Resp.ContentStream;
Reader := TStreamReader.Create(Stream, TEncoding.UTF8, True, 4096, False);
try
while not Reader.EndOfStream do
begin
Line := Reader.ReadLine;
if Line = “ then
Continue;

// Format SSE: linji bħal „data: {…}“ jew „data: [DONE]“
if Line.StartsWith(‚data:‘) then
begin
Data := Line.Substring(5).Trim;
if SameText(Data, ‚[DONE]‘) then
begin
TThread.Queue(nil,
procedure
begin
AOnEvent(“, True);
end);
Break;
end;

// Evalwazzjoni ta‘ chunk JSON: choices[0].delta.content
J := TJSONObject.ParseJSONValue(Data);
try
ContentChunk := “;
if J <> nil then
begin
Choices := (J as TJSONObject).GetValue(‚choices‘);
if (Choices is TJSONArray) and (TJSONArray(Choices).Count > 0) then
begin
Choice0 := TJSONArray(Choices).Items[0];
Delta := (Choice0 as TJSONObject).GetValue(‚delta‘);
if (Delta is TJSONObject) then
ContentChunk := TJSONObject(Delta).GetValue<string>(‚content‘, “);
end;
end;
finally
J.Free;
end;

if ContentChunk <> “ then
TThread.Queue(nil,
procedure
begin
AOnEvent(ContentChunk, False);
end);
end;
end;
finally
Reader.Free;
end;
finally
Payload.Free;
end;
finally
Body.Free;
end;
finally
Http.Free;
end;
end);
end;

end.

Għal xiex jservi dan il-approċċ

Il-kodiċi jsolvi tliet klassijiet tipici ta‘ problemi li f’VCL/FMX spiss jistgħu jwasslu għal kosti għoljin:

  • Streaming mingħajr tfixkil tal-UI: Il-HTTP-stream jinqara fil-isfond; l-aġġornamenti tal-UI jiġu eżegwiti permezz ta‘ TThread.Queue (asynkron fil-Main Thread). Dan hu l-metodu robust u standard f’FMX u VCL.
  • Tentattivi mill-ġdid għal żbalji tan-netwerk: Fil-każ ta‘ ENetHTTPClientException jiġi pprovat mill-ġdid b’mekkaniżmu ta‘ backoff. Dan hu skont intenzjonat sempliċi u jista‘ jiġi estiż aktar tard sabiex jinkludi status codes (429/5xx).
  • Parsing robust tal-JSON: Minflok tgħaddi b’mod blind il-casts fuq il-kampijiet, isir verifika pass pass. Dan inaqqas l-iżbalji „Invalid type cast“ f’responsi speċjali.

Kundizzjonijiet, perikli u varjanti

  • SSE mhux „normali JSON“: Fil-streaming jaslu serje ta‘ eventi, mhux risposta JSON waħda. Għalhekk il-qari linja b’linja u r-rikonoxximent ta‘ [DONE] huma ċentrali.
  • THTTPClient u Proxies/SSL: F’netwerks ta‘ amministrazzjoni hemm TLS-Inspection u obbligi ta‘ proxy. Ippjana THTTPClient.ProxySettings u, fejn xieraq, kwistjonijiet ta‘ ċertifikati. Debugging: dejjem illoggja s-statuscode u l-headers (mingħajr API-Key).
  • Strategija ta‘ Timeout: ResponseTimeout huwa delikat fil-streaming: jekk tagħżel perjodu qasir wisq, il-client jinqata‘ risposti twal. F’għodod tal-UI huwa spiss aktar raġonevoli timeout itwal u buttuna „Ikkanċella“ milli „qosor u strett“.
  • Terminazzjoni tat-Thread: Il-kodiċi ma juri l-ebda Cancel-Token. Għal għodod produttivi, jiswew mekkaniżmu ta‘ terminazzjoni (eż. flag + Http.CancelAll f’verżjonijiet aktar ġodda ta‘ Delphi jew permezz ta‘ terminazzjoni kontrollata tal-stream).
  • Evoluzzjoni tal-mudell u tal-API: Is-struttura tar-responsi tista‘ tinbidel. Żomm il-parsers difensivi u ċentrali, mhux imqassmin fil-kodiċi tal-formoli.

Debugging f’klijenti ta‘ Delphi li evolwew: X’għandek verament tilloggja

F’proġetti ta‘ integrazjoni, l-ewwel inizzjalizzazzjoni spiss ma tfalli xejn minħabba l-JSON, iżda minħabba dettalji tal-ambjent. F’log tekniku (file, Eventlog, logger ċentrali) prattikament għandu jkun:

  • Request-ID (assegnata minnek), Timestamp, URL tal-mira (mingħajr query strings li fihom sigrieti).
  • HTTP-Statuscode, Content-Type, tul tar-risposta, żmien tal-eżekuzzjoni.
  • Response-Body imnaqqas f’każ ta‘ żball (eż. massimu 4–8 KB), sabiex tkun tista‘ tirrikonoxxi żbalji ta‘ quota/policy.
  • Markar esplicitu tat-tentattivi mill-ġdid: tentattiv, dewmien, klassi tal-exception.

Il-API-Key qatt m’għandux jiddaħħal fil-log. Jekk tilloggja r-Request-Body, agħmel dan biss f’builds ta‘ dijanjosi u b’maskar, għax il-prompts jistgħu jinkludu kontenut personali jew kummerċjali.

Ħarsa għall-sitwazzjonijiet legacy: VCL, FMX u Layer-3 arkitettura

Ħafna applikazzjonijiet ta‘ Delphi jaħdmu f’logika klassika ta‘ 3-saffi („Layer-3 arkitettura“: UI, logika tan-negozju, data/integrazzjoni). Għal il-konnessjoni ma‘ ChatGPT dan hu utli: il-klijent murija għandu jkun fis-saff tal-integrazzjoni; il-logika tan-negozju tiddeċiedi, x‘ hu mitlub; l-UI turi biss il-istorja u s-status. B’hekk tevita li bidla fil-futur (provider ieħor, On-Prem-Proxy, endpoints ġodda) taffettwa negattivament il-forms.

Anke għall-modernizzazzjoni ta‘ Delphi dan hu punt tajjeb ta‘ bidu: l-ewwel klijent stabbli, imbagħad titjib tal-UI (streaming, Ikkanċella, storja), u mbagħad funzjonijiet „aktar intelliġenti“ bħal risposti strutturati jew sejħiet ta‘ għodod.

Konklużjoni: Bażi solida, iżda mhux kull applikazzjoni għandha bżonn Streaming

Il-konnessjoni tal-ChatGPT API ma‘ Delphi FMX/VCL b’mod nadif hija ta‘ valur partikolari fejn ir-reżpons tal-UI, is-sigurtà tal-operazzjoni u l-faċilità ta‘ debugging huma importanti: strumenti għall-amministrazzjoni, klijenti tad-desktop viċin tal-proċess jew għodod ta‘ appoġġ f’soluzzjonijiet diġitali għall-kumpaniji. Is-snippet murija huwa b’mod kunċettwalment pragmatiku: SSE-Streaming mingħajr libreriji speċjali, retry biss għal żbalji reali tan-netwerk, JSON parsjat b’mod difensiv.

Limitazzjonijiet tal-użu: Jekk għandek rekwiżiti stretti ta‘ compliance, governance ċentrali tal-prompt, appoġġ għal multi-tenancy jew audit trails dettaljati, spiss „client fuq id-desktop“ mhux biżżejjed. F’dawk il-każi, il-konnessjoni tipikament għandha tkun f’server kontrollat (pereżempju servizz proprju REST), li jimplimenta policies, logging u kontroll tal-aċċess b’mod ċentrali. Għal ħafna installazzjonijiet Delphi il-client murija hawn huwa punt ta‘ bidu robust li jista‘ jittrasforma pass pass f’arkitettura globali aktar nadifa.

Fil-qasam professjonali, l-Openai API f’Delphi u l-HTTP client timeout u retry f’Delphi għandhom ukoll rwol importanti, meta l-integrazzjonijiet, il-flussi ta‘ data u l-iżvilupp kontinwu għandhom jaħdmu flimkien b’mod nadif.

Tiddiskuti proġett jew inizjattiva ta‘ modernizzazzjoni ma‘ Net-Base.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

Aħna nappoġġjaw mhux biss f'kwistjonijiet puntwali, iżda wkoll meta biċċiet ta' kodiċi sors, temi legacy jew ideat għal portali jridu jsiru proġett korporattiv stabbli u affidabbli.

  • L-istat attwali, l-istat tal-mira u r-riskji tekniċi jiġu vvalutati flimkien.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Aqsam il-post

Aqsam dan il-post direttament

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

Imejl

Instagram jiftaħ f'tab ġdid. Il-link u t-test qasir jiġu kkopjati qabel fil-clipboard.