Test-Apps für Gemini Api und Blockly

This commit is contained in:
Michael Schimmel
2025-06-10 17:08:09 +02:00
parent 312be15cc8
commit 0e598d595e
8 changed files with 1555 additions and 35 deletions
+108
View File
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Blockly zu Umgangssprache - Mit Schleifen & Zählern</title>
<style>
body { font-family: sans-serif; }
#container { display: flex; }
#outputArea { margin-left: 20px; }
#codeOutput { width: 400px; height: 580px; font-size: 14px; white-space: pre-wrap; border: 1px solid #ccc; }
#blocklyDiv { height: 600px; width: 800px; }
.button-bar { margin-bottom: 10px; }
</style>
</head>
<body>
<h1>Mein "Umgangssprache"-Generator</h1>
<p>Bauen Sie links Ihre Blöcke. Rechts erscheint die textliche Beschreibung.</p>
<div class="button-bar">
<button onclick="saveWorkspace()">Speichern</button>
<button onclick="loadWorkspace()">Letzten Stand laden</button>
</div>
<div id="container">
<div id="blocklyDiv"></div>
<div id="outputArea">
<h2>Erzeugter Text:</h2>
<textarea id="codeOutput" readonly></textarea>
</div>
</div>
<script src="https://unpkg.com/blockly/blockly_compressed.js"></script>
<script src="https://unpkg.com/blockly/blocks_compressed.js"></script>
<script src="https://unpkg.com/blockly/msg/de.js"></script>
<script>
window.onload = function() {
let workspace;
// === UNSER UMGANGSSPRACHE-GENERATOR ===
Blockly.Human = new Blockly.Generator('Human');
Blockly.Human.ORDER_ATOMIC = 0;
Blockly.Human.scrub_ = function(block, code, opt_thisOnly) { const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); const nextCode = opt_thisOnly ? '' : Blockly.Human.blockToCode(nextBlock); return code + nextCode; };
Blockly.Human.prefixLines = function(text, prefix) { return prefix + text.split(/\n(?!$)/).join('\n' + prefix); };
// --- Generatoren für die einzelnen Blöcke ---
Blockly.Human.forBlock['controls_if'] = function(block) { let conditionCode = Blockly.Human.valueToCode(block, 'IF0', Blockly.Human.ORDER_ATOMIC) || 'eine Bedingung erfüllt ist'; let branchCode = Blockly.Human.statementToCode(block, 'DO0') || 'nichts.\n'; let code = `Also, überprüfe, ${conditionCode}.\n`; code += `Wenn das zutrifft, dann mach Folgendes:\n`; code += Blockly.Human.prefixLines(branchCode, ' '); return code; };
Blockly.Human.forBlock['logic_compare'] = function(block) { const OPERATORS = {'EQ': 'ist gleich', 'NEQ': 'ist nicht gleich', 'LT': 'ist kleiner als', 'LTE': 'ist kleiner oder gleich', 'GT': 'ist größer als', 'GTE': 'ist größer oder gleich'}; const operator = OPERATORS[block.getFieldValue('OP')]; const value_a = Blockly.Human.valueToCode(block, 'A', Blockly.Human.ORDER_ATOMIC) || 'irgendetwas'; const value_b = Blockly.Human.valueToCode(block, 'B', Blockly.Human.ORDER_ATOMIC) || 'irgendetwas anderes'; const code = `ob der Wert von ${value_a} ${operator} als ${value_b}`; return [code, Blockly.Human.ORDER_ATOMIC]; };
Blockly.Human.forBlock['math_number'] = function(block) { return [String(block.getFieldValue('NUM')), Blockly.Human.ORDER_ATOMIC]; };
Blockly.Human.forBlock['variables_get'] = function(block) { const varId = block.getFieldValue('VAR'); const variable = block.workspace.getVariableById(varId); const varName = variable ? variable.name : 'unbekannt'; return [`der Variable "${varName}"`, Blockly.Human.ORDER_ATOMIC]; };
Blockly.Human.forBlock['variables_set'] = function(block) { const varId = block.getFieldValue('VAR'); const variable = block.workspace.getVariableById(varId); const varName = variable ? variable.name : 'unbekannt'; const value = Blockly.Human.valueToCode(block, 'VALUE', Blockly.Human.ORDER_ATOMIC) || 'nichts'; return `Setze die Variable "${varName}" auf den Wert ${value}.\n`; };
Blockly.Human.forBlock['text_print'] = function(block) { const value = Blockly.Human.valueToCode(block, 'TEXT', Blockly.Human.ORDER_ATOMIC) || '"nichts"'; return `Gib auf dem Bildschirm aus: ${value}\n`; };
Blockly.Human.forBlock['text'] = function(block) { return [`"${block.getFieldValue('TEXT')}"`, Blockly.Human.ORDER_ATOMIC]; };
Blockly.Human.forBlock['controls_repeat_ext'] = function(block) { const repeats = Blockly.Human.valueToCode(block, 'TIMES', Blockly.Human.ORDER_ATOMIC) || 'einige Male'; const branch = Blockly.Human.statementToCode(block, 'DO') || 'mache nichts.\n'; let code = `Wiederhole die folgenden Schritte ${repeats} Mal:\n`; code += Blockly.Human.prefixLines(branch, ' '); return code; };
// NEU: Generator für den "ändere Variable um"-Block
Blockly.Human.forBlock['math_change'] = function(block) {
const varId = block.getFieldValue('VAR');
const variable = block.workspace.getVariableById(varId);
const varName = variable ? variable.name : 'unbekannt';
const delta = Blockly.Human.valueToCode(block, 'DELTA', Blockly.Human.ORDER_ATOMIC) || '0';
return `Ändere die Variable "${varName}" um den Wert ${delta}.\n`;
};
// === INITIALISIERUNG VON BLOCKLY ===
const toolboxXml = `
<xml>
<category name="Logik" colour="%{BKY_LOGIC_HUE}">
<block type="controls_if"></block>
<block type="logic_compare"></block>
</category>
<category name="Schleifen" colour="%{BKY_LOOPS_HUE}">
<block type="controls_repeat_ext">
<value name="TIMES"><shadow type="math_number"><field name="NUM">10</field></shadow></value>
</block>
</category>
<category name="Text" colour="%{BKY_TEXTS_HUE}">
<block type="text_print"></block>
<block type="text"></block>
</category>
<category name="Mathematik" colour="%{BKY_MATH_HUE}">
<block type="math_number"></block>
<block type="math_change">
<value name="DELTA"><shadow type="math_number"><field name="NUM">1</field></shadow></value>
</block>
</category>
<category name="Variablen" colour="%{BKY_VARIABLES_HUE}" custom="VARIABLE"></category>
</xml>`;
workspace = Blockly.inject('blocklyDiv', {
toolbox: toolboxXml
});
// === Funktionen für Speichern und Laden (unverändert) ===
window.saveWorkspace = function() { const state = Blockly.serialization.workspaces.save(workspace); localStorage.setItem('blocklyWorkspace', JSON.stringify(state)); alert('Arbeitsbereich gespeichert!'); }
window.loadWorkspace = function() { const stateString = localStorage.getItem('blocklyWorkspace'); if (!stateString) { return; } const state = JSON.parse(stateString); Blockly.serialization.workspaces.load(state, workspace); }
function updateCode(event) { if (event.type == Blockly.Events.UI) return; const code = Blockly.Human.workspaceToCode(workspace); document.getElementById('codeOutput').value = code; }
workspace.addChangeListener(updateCode);
loadWorkspace();
};
</script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
unit ApiKey;
interface
const Value = 'AIzaSyALccoMxn0_wcHswavhu5rzdglLeH6gVlI';
implementation
end.
+16
View File
@@ -0,0 +1,16 @@
program GeminiAccess;
uses
FastMM5,
Vcl.Forms,
MainUnit in 'MainUnit.pas' {Form1},
ApiKey in 'ApiKey.pas';
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+52
View File
@@ -0,0 +1,52 @@
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 1011
ClientWidth = 1105
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
OnCreate = FormCreate
OnDestroy = FormDestroy
DesignSize = (
1105
1011)
TextHeight = 15
object AnswerMemo: TMemo
Left = 0
Top = 0
Width = 1105
Height = 809
Anchors = [akLeft, akTop, akRight, akBottom]
ScrollBars = ssVertical
TabOrder = 0
end
object ExecButton: TButton
Left = 992
Top = 928
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Caption = 'Execute'
TabOrder = 1
OnClick = ExecButtonClick
end
object PromptMemo: TMemo
Left = 0
Top = 815
Width = 1105
Height = 89
Anchors = [akLeft, akRight, akBottom]
ScrollBars = ssVertical
TabOrder = 2
end
object ApplicationEvents: TApplicationEvents
OnIdle = ApplicationEventsIdle
Left = 136
Top = 104
end
end
+204
View File
@@ -0,0 +1,204 @@
unit MainUnit;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
System.Net.HttpClient, System.Net.HttpClientComponent, System.JSON, System.Net.Mime, Vcl.AppEvnts,
Myc.Futures, System.Generics.Collections, ApiKey;
type
// NEU: Ein Record, um einen einzelnen Redebeitrag in der Konversation zu speichern
TChatTurn = record
Role: string; // 'user' oder 'model'
Text: string;
end;
TForm1 = class(TForm)
AnswerMemo: TMemo;
ExecButton: TButton;
ApplicationEvents: TApplicationEvents;
PromptMemo: TMemo;
procedure ExecButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ApplicationEventsIdle(Sender: TObject; var Done: Boolean);
private
FHttpClient: TNetHTTPClient;
FMyApiKey: string;
FAnswer: TFuture<String>;
FChatHistory: TList<TChatTurn>; // NEU: Liste für den Konversationsverlauf
procedure SendPrompt;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
const
GEMINI_MODEL = 'gemini-2.5-flash-preview-05-20';
API_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/models/';
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
FHttpClient := TNetHTTPClient.Create(nil);
FMyApiKey := ApiKey.Value; // Eingelesen aus ApiKey.pas
// NEU: Initialisiert die Liste für den Konversationsverlauf
FChatHistory := TList<TChatTurn>.Create;
// Optional: Füge hier einen initialen System-Prompt hinzu, wenn gewünscht.
// Dieser wird dann immer als erste Nachricht mitgesendet.
// var initialTurn: TChatTurn;
// initialTurn.Role := 'user';
// initialTurn.Text := 'Du bist ein Delphi-Entwicklungs-Assistent...';
// FChatHistory.Add(initialTurn);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FAnswer.WaitFor;
FHttpClient.Free;
FChatHistory.Free; // NEU: Gibt die Verlaufsliste frei
end;
procedure TForm1.ApplicationEventsIdle(Sender: TObject; var Done: Boolean);
var
answerText: string;
modelTurn: TChatTurn;
begin
if FAnswer.Done.IsSet and not ExecButton.Enabled then
begin
answerText := FAnswer.Value;
AnswerMemo.Lines.Add(answerText);
// NEU: Füge die Antwort des Modells zum Verlauf hinzu
if not answerText.StartsWith('Fehler!') then
begin
modelTurn.Role := 'model';
modelTurn.Text := answerText;
FChatHistory.Add(modelTurn);
end;
FAnswer := FAnswer.Null;
ExecButton.Enabled := True;
end;
end;
procedure TForm1.ExecButtonClick(Sender: TObject);
begin
SendPrompt;
end;
procedure TForm1.SendPrompt;
var
userPrompt: string;
userTurn: TChatTurn;
begin
if not ExecButton.Enabled then
Exit;
userPrompt := PromptMemo.Lines.Text;
if userPrompt.IsEmpty then
begin
ShowMessage('Bitte geben Sie einen Prompt ein.');
Exit;
end;
// NEU: Füge die aktuelle Benutzereingabe zum Verlauf hinzu
userTurn.Role := 'user';
userTurn.Text := userPrompt;
FChatHistory.Add(userTurn);
// NEU: Leere das Eingabefeld für die nächste Nachricht
PromptMemo.Clear;
ExecButton.Enabled := False;
AnswerMemo.Lines.Add('');
AnswerMemo.Lines.Add('--- [USER] ---');
AnswerMemo.Lines.Add(userPrompt);
AnswerMemo.Lines.Add('... sende Anfrage an Gemini ...');
FAnswer := TFuture<string>.Construct(
function: string
var
jsonRequest: TJSONObject;
jsonContents: TJSONArray;
requestBody: TStringStream;
response: IHTTPResponse;
responseJson: TJSONValue;
apiUrl: string;
turn: TChatTurn;
begin
try
// 1. JSON-Body aus dem gesamten Konversationsverlauf erstellen
jsonRequest := TJSONObject.Create;
try
jsonContents := TJSONArray.Create;
jsonRequest.AddPair('contents', jsonContents);
// NEU: Iteriere durch den Verlauf und baue die JSON-Struktur auf
for turn in FChatHistory do
begin
var jsonTurn := TJSONObject.Create;
jsonTurn.AddPair('role', TJSONString.Create(turn.Role));
var jsonParts := TJSONArray.Create;
jsonTurn.AddPair('parts', jsonParts);
var jsonText := TJSONObject.Create;
jsonText.AddPair('text', TJSONString.Create(turn.Text));
jsonParts.Add(jsonText);
jsonContents.Add(jsonTurn);
end;
// 2. Request senden
requestBody := TStringStream.Create(jsonRequest.ToString, TEncoding.UTF8);
try
apiUrl := API_BASE_URL + GEMINI_MODEL + ':generateContent?key=' + FMyApiKey;
response := FHttpClient.Post(apiUrl, requestBody, nil);
// 3. Antwort verarbeiten
if (response.StatusCode = 200) then
begin
responseJson := TJSONObject.ParseJSONValue(response.ContentAsString(TEncoding.UTF8));
try
Result := responseJson.GetValue<TJSONArray>('candidates')
.Items[0].GetValue<TJSONObject>('content')
.GetValue<TJSONArray>('parts')
.Items[0].GetValue<string>('text');
finally
responseJson.Free;
end;
end
else
begin
Result := 'Fehler! Status: ' + response.StatusCode.ToString + ' - ' + response.StatusText +
sLineBreak + response.ContentAsString;
end;
finally
requestBody.Free;
end;
finally
jsonRequest.Free;
end;
except
on E: Exception do
begin
Result := '--- FEHLER IM TASK ---' + sLineBreak + E.Message;
end;
end;
end);
end;
end.
+36 -35
View File
@@ -5,7 +5,7 @@ unit Myc.Trade.DataStream;
Provides a data server for loading time-series data and streaming it.
This unit contains the core components for handling historical data:
- TDataRecord: A generic record for a single time-stamped data entry.
- TAuraFileDataRecord: A generic record for a single time-stamped data entry.
- IDataServer/TAuraDataServer: A server component responsible for loading and caching
series of data files from disk. Each server instance manages its own cache,
but all instances share a central load gate.
@@ -29,11 +29,6 @@ uses
Myc.Trade.DataPoint;
type
// A generic record for a single time-stamped data entry.
TDataRecord<T: record> = packed record
TimeStamp: TDateTime;
Data: T;
end;
// Represents a generic data stream capable of providing sequential data chunks.
IDataStream<T> = interface
@@ -69,10 +64,16 @@ type
procedure ClearCache;
end;
// A generic record for a single time-stamped data entry.
TAuraFileDataRecord<T: record> = packed record
TimeStamp: TDateTime;
Data: T;
end;
IAuraDataServer<T: record> = interface(IDataServer<T>)
['{481E27DE-DC58-49AF-B8A8-B6316980C423}']
function LoadDataFile(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function LoadDataFile(const FileName: string): TFuture<TArray<TAuraFileDataRecord<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TAuraFileDataRecord<T>>>;
function EnumerateAssetFiles: TArray<String>;
end;
@@ -87,7 +88,7 @@ type
Name: String;
Age: TDateTime;
LastUsed: TDateTime;
Data: TFuture<TArray<TDataRecord<T>>>;
Data: TFuture<TArray<TAuraFileDataRecord<T>>>;
end;
private
// The cache is per-instance.
@@ -101,8 +102,8 @@ type
FLoadGate: TLatch;
protected
class function ReadCompressedData(const InputStream: TStream): TArray<TDataRecord<T>>; static;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataRecord<T>>; static;
class function ReadCompressedData(const InputStream: TStream): TArray<TAuraFileDataRecord<T>>; static;
class function ReadUncompressedData(const InputStream: TStream): TArray<TAuraFileDataRecord<T>>; static;
public
constructor Create(const APath: String);
@@ -123,8 +124,8 @@ type
procedure ClearCache;
function EnumerateAssetFiles: TArray<String>;
function LoadDataFile(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function LoadDataFile(const FileName: string): TFuture<TArray<TAuraFileDataRecord<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TAuraFileDataRecord<T>>>;
property Path: String read GetPath;
end;
@@ -135,9 +136,9 @@ type
FDataServer: TAuraDataServer<T>;
FIsLiveData: TMutable<Boolean>.IWriteable;
FCurrentFileName: string;
FCurrentData: TFuture<TArray<TDataRecord<T>>>;
FCurrentData: TFuture<TArray<TAuraFileDataRecord<T>>>;
FNextFileName: string;
FNextData: TFuture<TArray<TDataRecord<T>>>;
FNextData: TFuture<TArray<TAuraFileDataRecord<T>>>;
FCurrPosInFile: Int64;
FCurrentIdx: Int64;
FLastTimeStamp: TDateTime;
@@ -314,12 +315,12 @@ begin
Result := FPath;
end;
function TAuraDataServer<T>.LoadDataFile(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function TAuraDataServer<T>.LoadDataFile(const FileName: string): TFuture<TArray<TAuraFileDataRecord<T>>>;
var
parsedPath, parsedSymbol: string;
parsedYear, parsedMonth: Integer;
begin
Result := TFuture<TArray<TDataRecord<T>>>.Null;
Result := TFuture<TArray<TAuraFileDataRecord<T>>>.Null;
if not TryParseFileName(FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
exit;
@@ -368,8 +369,8 @@ begin
.Construct(
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
.Chain<TArray<TDataRecord<T>>>(
function(bytes: TBytes): TArray<TDataRecord<T>>
.Chain<TArray<TAuraFileDataRecord<T>>>(
function(bytes: TBytes): TArray<TAuraFileDataRecord<T>>
begin
var zipMemoryStream := TBytesStream.Create(bytes);
try
@@ -383,9 +384,9 @@ begin
else if TFile.Exists(FileName) then
begin
Result :=
TFuture<TArray<TDataRecord<T>>>.Construct(
TFuture<TArray<TAuraFileDataRecord<T>>>.Construct(
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
function: TArray<TDataRecord<T>>
function: TArray<TAuraFileDataRecord<T>>
begin
if TFile.Exists(capFileName) then
begin
@@ -418,11 +419,11 @@ begin
end;
end;
function TAuraDataServer<T>.LoadDataSeries(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function TAuraDataServer<T>.LoadDataSeries(const FileName: string): TFuture<TArray<TAuraFileDataRecord<T>>>;
var
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataRecord<T>>>>;
liveData: TFuture<TArray<TDataRecord<T>>>;
loadedFiles: TArray<TFuture<TArray<TAuraFileDataRecord<T>>>>;
liveData: TFuture<TArray<TAuraFileDataRecord<T>>>;
tabFiles: TList<string>;
currentFile: string;
liveFilePath: string;
@@ -452,7 +453,7 @@ begin
end;
end;
var loadedFileList := TList<TFuture<TArray<TDataRecord<T>>>>.Create;
var loadedFileList := TList<TFuture<TArray<TAuraFileDataRecord<T>>>>.Create;
var loadStates := TList<TState>.Create;
try
for currentFile in tabFiles do
@@ -462,7 +463,7 @@ begin
loadStates.Add(data.Done);
end;
liveData := TFuture<TArray<TDataRecord<T>>>.Null;
liveData := TFuture<TArray<TAuraFileDataRecord<T>>>.Null;
if liveFilePath <> '' then
begin
liveData := LoadDataFile(liveFilePath);
@@ -481,11 +482,11 @@ begin
end;
Result :=
TFuture<TArray<TDataRecord<T>>>.Construct(
TFuture<TArray<TAuraFileDataRecord<T>>>.Construct(
loadedState,
function: TArray<TDataRecord<T>>
function: TArray<TAuraFileDataRecord<T>>
begin
var tickList := TList<TDataRecord<T>>.Create;
var tickList := TList<TAuraFileDataRecord<T>>.Create;
try
var overallLastTabTickTime: TDateTime := 0;
var cnt := 0;
@@ -508,7 +509,7 @@ begin
);
end;
class function TAuraDataServer<T>.ReadCompressedData(const InputStream: TStream): TArray<TDataRecord<T>>;
class function TAuraDataServer<T>.ReadCompressedData(const InputStream: TStream): TArray<TAuraFileDataRecord<T>>;
var
decompressionStream: TStream;
localHeader: TZipHeader;
@@ -547,7 +548,7 @@ begin
end;
end;
class function TAuraDataServer<T>.ReadUncompressedData(const InputStream: TStream): TArray<TDataRecord<T>>;
class function TAuraDataServer<T>.ReadUncompressedData(const InputStream: TStream): TArray<TAuraFileDataRecord<T>>;
var
fileSize: Int64;
recordCount, bytesRead: Integer;
@@ -555,10 +556,10 @@ begin
SetLength(Result, 0);
InputStream.Position := 0;
fileSize := InputStream.Size;
if (fileSize = 0) or ((fileSize mod SizeOf(TDataRecord<T>)) <> 0) then
if (fileSize = 0) or ((fileSize mod SizeOf(TAuraFileDataRecord<T>)) <> 0) then
exit;
recordCount := fileSize div SizeOf(TDataRecord<T>);
recordCount := fileSize div SizeOf(TAuraFileDataRecord<T>);
if recordCount > 0 then
begin
SetLength(Result, recordCount);
@@ -596,8 +597,8 @@ end;
function TAuraFileStream<T>.GetChunk(var Data: array of TDataPoint<T>): Integer;
var
item: TDataRecord<T>;
currData: TArray<TDataRecord<T>>;
item: TAuraFileDataRecord<T>;
currData: TArray<TAuraFileDataRecord<T>>;
begin
Result := 0;
if not FCurrentData.Done.IsSet then