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.

Pass li jmiss

Meta suġġett jiġi mwettaq bħala proġett reali, l-arkitettura, is-sistema eżistenti u l-operat għandhom jiġu kkunsidrati flimkien kmieni.

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, aċċess tad-dejta, portalijiet u rollout ma jiġu posposti bħala konsegwenzi tardivi.
  • Tara kmieni liema triq hija ekonomika u operattivament sostenibbli.

Aqsam il-post

Aqsam dan il-post direttament

LinkedIn, X, XING, Facebook, WhatsApp u E-Mail huma disponibbli immedjatament. Għal Instagram nippreparaw il-link u test qasir direttament.

Imejl

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