Nga tema e revistës në praktikën e projektit
Faqe shërbimi dhe teknike të përshtatshme për artikullin
Pse „ChatGPT API me Delphi FMX/VCL“ në praktikë nuk është vetëm një POST
Kushdo që dëshiron të lidhë ChatGPT API me Delphi FMX/VCL, shpejt përfundon te një HTTP-POST i thjeshtë. Në mjedise reale të softuerit biznesor kjo bie në tre pika: (1) Timeout-et dhe Retry-t duhet të jenë deterministikë, sepse përndryshe përdoruesit përjetojnë UI „të bllokuar“, (2) Streaming (Server-Sent Events, shkurt SSE) shpesh është i nevojshëm për UX të mirë, por në Delphi-Threading bëhet shpejt i prirur për gabime, dhe (3) JSON nuk është vetëm „një objekt“: mesazhet e gabimit, problemet me kuotat, fushat bosh ose format e përgjigjeve lehtësisht të ndryshuara duhet të trajtohen në mënyrë robuste.
Shembulli i mëposhtëm i kodit tregon një qasje që funksionon njësoj në FMX dhe VCL: Një klient i vetëdijshëm dhe i testueshëm, i cili punon alternativisht në modalitetin nicht-streaming ose streaming, marshalon përditësimet e UI në mënyrë të pastër (pra i ekzekuton përmes sinkronizimit në Main Thread) dhe regjistron gabimet me mesazhe të qarta. Së bashku është ndërtuar në mënyrë që të përshtatet në struktura layer të zhvilluara (p.sh. „API-Client“ në shtresën e integrimit, UI mbetet e hollë).
Architektur-Skizze: UI entkoppeln, Client testbar halten
Në projekte Delphi me histori të gjatë shpesh shohim „HTTP im ButtonClick“. Kjo funksionon deri te incidenti i parë. E rekomandueshme është një klient i vogël me:
- Konfiguration: Base-URL, API-Key, model, timeout-et.
- Transportschicht: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-Optionen (sipas nevojave të operimit).
- Parser: JSON-Decoding, objekte gabimesh, nxjerrja e rezultatit.
- UI-Hooks: Callback për Token-/Text-Streaming, por pa varësi të fortë nga VCL/FMX Controls.
Në këtë mënyrë integrimi në softuerin individual të ndërmarrjes mund të menaxhohet qartë: Klienti ripërdoret në shërbime, klientë desktop, vegla admin ose test-harnesses.
Source-Schnipsel: Delphi-Client mit SSE-Streaming, Timeout/Retry und robustem JSON
Kodi përdor THTTPClient (System.Net.HttpClient) dhe e parson qëllimisht vetëm minimalisht me System.JSON. Për SSE lexojmë rresht pas rreshti dhe reagojmë ndaj „data: …“. Kjo nuk është „WebSocket“, por një HTTP-Response-Stream që furnizon vazhdimisht rreshta teksti. E rëndësishme: Ne lexojmë në një Worker-Thread dhe marshalojmë përditësimet e UI në 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: këtu API-Key si „Authorization: Bearer …“.
// Në mjedise korporative, kujdes që çelësat të mos përfshihen në 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));
// Struktura minimale e 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
// Formë e zakonshme: { „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(‚Përgjigje JSON e papritur (jo objekt).‘, 0, AJsonText);
Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Përgjigje JSON e papritur: choices mungon/është bosh.‘, 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(‚Përgjigje JSON e papritur: message mungon.‘, 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
// Gabim rrjeti/TLS/timeout: riprovim i thjeshtë me 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(‚Gabim 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
// Nisja e stream-it në mënyrë të ndërgjegjshme asinkrone, në mënyrë që FMX/VCL të mos bllokohet.
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
// Në rast gabimesh streaming, përmbajtja shpesh është përsëri JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Nisja e streaming-ut dështoi (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;
// Formati SSE: rreshta si „data: {…}“ ose „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;
// Vlerëso 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.
Për çfarë është e dobishme kjo qasje
Kodi zgjidh tre klasa tipike problemesh që në VCL/FMX shpejt bëhen të kushtueshme:
- Streaming pa ngrirje të UI-së: HTTP-Stream-i lexohet në sfond; përditësimet e UI-së kryhen përmes
TThread.Queue(asincron në Main Thread). Kjo është rruga standarde e qëndrueshme në FMX dhe VCL. - Retry për gabimet e rrjetit: Në rastin e
ENetHTTPClientExceptionbëhet riprovim me backoff. Kjo është qëllimisht e thjeshtë dhe mund të zgjerohet më vonë për kodet e statusit (429/5xx). - Parsimi i qëndrueshëm i JSON: Në vend që të bëhet cast-i verbër i fushave, kontrollohet hap pas hapi. Kjo zvogëlon gabimet „Invalid type cast“ në përgjigje të veçanta.
Kushtet, kurthet dhe variantet
- SSE nuk është „JSON i zakonshëm“: Në streaming vjen një sekuencë events, jo një përgjigje e vetme JSON. Prandaj leximi rresht për rresht dhe njohja e
[DONE]janë thelbësore. - THTTPClient dhe Proxies/SSL: Në rrjete administrative ekzistojnë TLS-Inspection dhe detyrimet për proxy. Parashikoni
THTTPClient.ProxySettingsdhe, nëse nevojitet, çështje certifikatash. Për debug: regjistroni gjithmonë HTTP-Statuscode dhe header-at (pa API-Key). - Strategjia e timeout-it:
ResponseTimeoutështë delikat në streaming: nëse zgjidhni të shkurtër, klienti do të presë përgjigjet e gjata. Në mjetet UI një timeout më i gjatë dhe një buton „Anulo“ shpesh janë më të përshtatshëm se „i shkurtër dhe i ashpër“. - Anullimi i thread-it: Fragmenti nuk tregon një Cancel-Token. Për mjetet produktive ia vlen të ketë një mekanizëm anulimi (p.sh. flag +
Http.CancelAllnë versionet e reja të Delphi ose përmes prerjes së kontrolluar të stream-it). - Zhvillimi i modelit dhe i API-së: Struktura e përgjigjeve mund të ndryshojë. Mbani parsuesin defansiv dhe qendror, jo të shpërndarë në kodin e formularit.
Debugging në klientë të zhvilluar Delphi: Çfarë duhet të regjistroni me të vërtetë
Në projektet e integrimit, nisja e parë rrallë dështon për shkak të JSON-it, por për shkak të detajeve të ambientit. Në një log teknik (skedar, Eventlog, logger qendror) në praktikë duhet të përfshihen:
- Request-ID (e gjeneruar vetë), timestamp, URL-ja e destinacionit (pa Secret-Querystrings).
- HTTP-Statuscode, Content-Type, gjatësia e përgjigjes, kohëzgjatja.
- Një Response-Body i shkurtuar në rast gabimi (p.sh. max. 4–8 KB), për të identifikuar gabime quota/policy.
- Shënim i qartë i përpjekjeve të riprovimit: Attempt, Delay, klasa e Exception-it.
API-Key nuk duhet kurrë të shkojë në log. Nëse regjistroni Request-Body, atëherë vetëm në build-e diagnostikuese dhe me maskim, sepse prompts mund të përmbajnë të dhëna personale ose përmbajtje biznesi.
Renditje për situata legacy: VCL, FMX dhe Layer-3 arkitekturë
Shumë aplikacione Delphi funksionojnë në një logjikë klasike me 3 shtresa („Layer-3 arkitekturë“: UI, logjika e biznesit, të dhënat/integrimi). Për lidhjen me ChatGPT kjo është e dobishme: klienti i treguar i takon shtresës së integrimit; logjika e biznesit vendos çfarë pyetet; UI-ja tregon vetëm historikun dhe statusin. Kështu shmangni që një ndryshim i mëvonshëm (provider tjetër, On-Prem-Proxy, endpoint-e të reja) të shkatërrojë formularët.
Edhe për modernizimin e Delphi kjo është një pikë e mirë hyrjeje: së pari një klient i qëndrueshëm, pastaj përmirësime të UI-së (Streaming, Anulo, historiku), dhe më pas veçori „më inteligjente“ si përgjigje të strukturuara ose thirrje të mjeteve.
Përfundim: Bazë e qëndrueshme, por jo çdo aplikacion ka nevojë për Streaming
Vlen veçanërisht të integrohet në mënyrë të pastër ChatGPT API me Delphi FMX/VCL aty ku reagimi i UI-së, siguria e operimit dhe aftësia për debug janë të rëndësishme: veglat e administrimit, klientët desktop afër procesit ose mjetet e suportit në zgjidhjet digjitale për biznes. Shembulli i treguar është qëllimisht pragmatik: SSE-Streaming pa biblioteka speciale, retry vetëm për gabime reale të rrjetit, JSON i analizuar në mënyrë defanzive.
Kufizimet e përdorimit: Nëse keni kërkesa të rrepta për përputhshmëri, qeverisje qendrore të prompt-eve, mbështetje për shumë klientë ose audit-trail-e të detajuara, zakonisht “një klient në desktop” nuk mjafton. Në atë rast, integrimi zakonisht duhet të vendoset në një server të kontrolluar (p.sh. një shërbim i vetë REST), i cili zbaton në mënyrë qendrore politika, logging dhe kontrollin e aksesit. Për shumë instalime Delphi klienti i treguar këtu është gjithsesi një pikë fillimi e besueshme, e cila mund të kalojë hap pas hapi në një arkitekturë më të pastër të përgjithshme.
Në mjedisin teknik luajnë gjithashtu rol të rëndësishëm Openai API In Delphi dhe Delphi Http Client Timeout Retry, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të bashkëveprojnë në mënyrë të pastër.
Diskutoni projektin ose iniciativën e modernizimit me Net-Base.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Ne nuk mbështesim vetëm në çështje të veçanta, por edhe kur nga fragmente të kodit burimor, temat legacy ose idetë për portale duhet të zhvillohen në një projekt korporativ të qëndrueshëm.
- Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.