Delphi
Exemplo com System.Net.HttpClient (THTTPClient) e FireDAC, sem componentes de terceiros. Funciona em Delphi 10.3 Rio ou mais novo, em aplicações VCL, FMX ou serviços.
Num aplicativo desktop instalado na máquina do usuário, qualquer pessoa pode extrair a API key do executável. Faça a chamada ao Datafala a partir de um servidor seu (um serviço DataSnap, Horse, RAD Server ou outra API) e deixe o aplicativo falar só com esse servidor. Veja Autenticação.
Cliente
unit Datafala.Client;
interface
uses
System.SysUtils, System.Classes, System.JSON, System.Hash,
System.Net.HttpClient, System.Net.URLClient;
type
EDatafala = class(Exception)
public
Status: Integer;
Code: string; // code estável da API: sql_rejected, no_answer, rate_limited...
RetryAfter: Integer; // segundos, quando a API manda Retry-After
RequestId: string;
end;
TDatafalaAnswer = record
Sql: string;
Params: TJSONObject; // é do chamador: libere com Params.Free
Dialect: string;
Explanation: string;
RequestId: string;
end;
TDatafalaClient = class
private
FBaseUrl, FApiKey, FEndUserSecret: string;
function Post(const Path: string; Body: TJSONObject): IHTTPResponse;
procedure RaiseError(const Response: IHTTPResponse);
public
constructor Create(const BaseUrl, ApiKey, EndUserSecret: string);
function EndUserId(const UserId: string): string;
function Ask(const Question, UserId: string; MaxRows: Integer = 0): TDatafalaAnswer;
procedure Feedback(const RequestId, Rating: string; const Comment: string = '');
end;
implementation
constructor TDatafalaClient.Create(const BaseUrl, ApiKey, EndUserSecret: string);
begin
inherited Create;
FBaseUrl := BaseUrl; // https://app.datafala.com.br
FApiKey := ApiKey; // leia de variável de ambiente ou cofre, nunca do código
FEndUserSecret := EndUserSecret; // segredo fixo do servidor; não troque depois
end;
function TDatafalaClient.EndUserId(const UserId: string): string;
begin
// HMAC-SHA256 do ID interno: 64 caracteres hexadecimais, sem dado pessoal.
Result := THashSHA2.GetHMAC(UserId, FEndUserSecret, THashSHA2.TSHA2Version.SHA256);
end;
function TDatafalaClient.Post(const Path: string; Body: TJSONObject): IHTTPResponse;
var
Http: THTTPClient;
Content: TStringStream;
begin
Http := THTTPClient.Create;
Content := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
try
Http.ConnectionTimeout := 5000;
Http.ResponseTimeout := 60000; // a LLM pode levar vários segundos
Http.ContentType := 'application/json';
Http.CustomHeaders['Authorization'] := 'Bearer ' + FApiKey;
Result := Http.Post(FBaseUrl + Path, Content);
finally
Content.Free;
Http.Free;
end;
end;
procedure TDatafalaClient.RaiseError(const Response: IHTTPResponse);
var
Body: TJSONValue;
Error: EDatafala;
begin
Body := TJSONObject.ParseJSONValue(Response.ContentAsString(TEncoding.UTF8));
try
if Body is TJSONObject then
Error := EDatafala.Create(TJSONObject(Body).GetValue<string>('message', ''))
else
Error := EDatafala.Create('HTTP ' + Response.StatusCode.ToString);
Error.Status := Response.StatusCode;
if Body is TJSONObject then
Error.Code := TJSONObject(Body).GetValue<string>('code', 'unknown')
else
Error.Code := 'unknown';
Error.RetryAfter := StrToIntDef(Response.HeaderValue['Retry-After'], 0);
Error.RequestId := Response.HeaderValue['X-Request-ID'];
finally
Body.Free;
end;
raise Error;
end;
function TDatafalaClient.Ask(const Question, UserId: string; MaxRows: Integer): TDatafalaAnswer;
var
Body, Json: TJSONObject;
Response: IHTTPResponse;
begin
Body := TJSONObject.Create;
try
Body.AddPair('question', Question);
Body.AddPair('end_user', EndUserId(UserId));
if MaxRows > 0 then
Body.AddPair('max_rows', TJSONNumber.Create(MaxRows));
Response := Post('/v1/ask', Body);
finally
Body.Free;
end;
if Response.StatusCode <> 200 then
RaiseError(Response);
Json := TJSONObject.ParseJSONValue(Response.ContentAsString(TEncoding.UTF8)) as TJSONObject;
try
Result.Sql := Json.GetValue<string>('sql');
Result.Dialect := Json.GetValue<string>('dialect');
// explanation pode vir null
if not Json.TryGetValue<string>('explanation', Result.Explanation) then
Result.Explanation := '';
Result.RequestId := Json.GetValue<string>('request_id');
Result.Params := Json.GetValue<TJSONObject>('params').Clone as TJSONObject;
finally
Json.Free;
end;
end;
procedure TDatafalaClient.Feedback(const RequestId, Rating, Comment: string);
var
Body: TJSONObject;
Response: IHTTPResponse;
begin
Body := TJSONObject.Create;
try
Body.AddPair('request_id', RequestId);
Body.AddPair('rating', Rating); // 'good' ou 'bad'
if Comment <> '' then
Body.AddPair('comment', Copy(Comment, 1, 500));
Response := Post('/v1/feedback', Body);
finally
Body.Free;
end;
if Response.StatusCode <> 204 then
RaiseError(Response);
end;
end.
Converter os placeholders para o FireDAC
O Datafala devolve %(nome)s e escapa todo % de literal como %%. O FireDAC usa :nome. Converta numa única passada, da esquerda para a direita, para que um %% dentro de um literal nunca seja confundido com placeholder:
unit Datafala.Sql;
interface
// %(nome)s -> :nome ; %% -> %
function ToFireDAC(const Sql: string): string;
implementation
uses
System.SysUtils;
function IsNameChar(C: Char; First: Boolean): Boolean;
begin
Result := CharInSet(C, ['A'..'Z', 'a'..'z', '_']) or (not First and CharInSet(C, ['0'..'9']));
end;
function ToFireDAC(const Sql: string): string;
var
I, J: Integer;
Builder: TStringBuilder;
begin
Builder := TStringBuilder.Create(Length(Sql));
try
I := 1;
while I <= Length(Sql) do
begin
if (Sql[I] = '%') and (I < Length(Sql)) and (Sql[I + 1] = '%') then
begin
Builder.Append('%');
Inc(I, 2);
end
else if (Sql[I] = '%') and (I < Length(Sql)) and (Sql[I + 1] = '(') then
begin
J := I + 2;
while (J <= Length(Sql)) and IsNameChar(Sql[J], J = I + 2) do
Inc(J);
if (J < Length(Sql)) and (J > I + 2) and (Sql[J] = ')') and (Sql[J + 1] = 's') then
begin
Builder.Append(':').Append(Copy(Sql, I + 2, J - I - 2));
I := J + 2;
end
else
raise Exception.Create('Placeholder inválido no SQL do Datafala');
end
else
begin
Builder.Append(Sql[I]);
Inc(I);
end;
end;
Result := Builder.ToString;
finally
Builder.Free;
end;
end;
end.
Executar com FireDAC
Use uma TFDConnection com um usuário somente leitura e transação só de leitura. Desligue as macros e as sequências de escape do FireDAC: o SQL do Datafala é SQL puro, e um &, ! ou { num literal não pode ser interpretado pelo FireDAC.
uses
System.JSON, Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Option,
Datafala.Client, Datafala.Sql;
procedure BindParams(Query: TFDQuery; Params: TJSONObject);
var
Pair: TJSONPair;
Param: TFDParam;
begin
for Pair in Params do
begin
Param := Query.ParamByName(Pair.JsonString.Value);
if Pair.JsonValue is TJSONNull then
begin
Param.DataType := ftString;
Param.Clear;
end
else if Pair.JsonValue is TJSONBool then
Param.AsBoolean := TJSONBool(Pair.JsonValue).AsBoolean
else if Pair.JsonValue is TJSONNumber then
begin
if Pair.JsonValue.Value.IndexOfAny(['.', 'e', 'E']) >= 0 then
Param.AsFloat := TJSONNumber(Pair.JsonValue).AsDouble
else
Param.AsLargeInt := TJSONNumber(Pair.JsonValue).AsInt64;
end
else
Param.AsString := Pair.JsonValue.Value; // datas chegam como texto ISO (2026-10-22)
end;
end;
function Perguntar(Client: TDatafalaClient; Conexao: TFDConnection;
const Pergunta, IdDoUsuario: string): TFDQuery;
var
Answer: TDatafalaAnswer;
begin
Answer := Client.Ask(Pergunta, IdDoUsuario);
try
Result := TFDQuery.Create(nil);
try
Result.Connection := Conexao;
Result.ResourceOptions.MacroCreate := False;
Result.ResourceOptions.MacroExpand := False;
Result.ResourceOptions.EscapeExpand := False;
Result.ResourceOptions.CmdExecMode := amCancelable;
Result.ResourceOptions.CmdExecTimeout := 5000; // ms
Result.SQL.Text := ToFireDAC(Answer.Sql);
BindParams(Result, Answer.Params);
Result.Open;
except
Result.Free;
raise;
end;
finally
Answer.Params.Free;
end;
end;
Configuração da conexão somente leitura:
// Firebird
FDConexaoLeitura.Params.DriverID := 'FB';
FDConexaoLeitura.Params.UserName := 'DATAFALA_RO'; // GRANT SELECT só nas tabelas cadastradas
FDConexaoLeitura.TxOptions.ReadOnly := True;
// PostgreSQL: DriverID 'PG'; MySQL: DriverID 'MySQL'; Firebird: DriverID 'FB'. O usuário deve ser só leitura.
Tratar erros
try
Query := Perguntar(Client, FDConexaoLeitura, EditPergunta.Text, IntToStr(UsuarioLogado.Id));
except
on E: EDatafala do
if (E.Code = 'no_answer') or (E.Code = 'sql_rejected') then
ShowMessage(E.Message) // mensagem já traduzida: peça para reformular
else if E.Code = 'rate_limited' then
ShowMessage(Format('Muitas perguntas. Tente de novo em %d s.', [E.RetryAfter]))
else
raise;
end;
Lembretes:
- Não repita automaticamente um
Askque estourou o tempo: cada tentativa gasta tokens da sua LLM. - Guarde
Answer.RequestIdpara enviar 👍/👎 comClient.Feedback(RequestId, 'good'). - Veja a lista completa de códigos em Erros.