feat: Gemini thought_signature korrekt lesen und zurücksenden
- thought_signature aus delta.tool_calls[n].extra_content.google.thought_signature lesen (korrekter Pfad) - ThoughtSignature-Feld in MessageRecord + DB-Migration (ALTER TABLE messages ADD COLUMN thought_signature) - SaveMessage speichert ThoughtSignature pro Tool-Call - OpenAiService: toolCallAccumulators Tuple um ThoughtSignature erweitert - Tool-Calls mit thought_signature werden mit extra_content.google.thought_signature zurückgesendet (nicht function.thought_signature) - ConvertMessage (History-Rebuild) setzt thought_signature ebenfalls korrekt - ChatService.BuildApiMessages übergibt thought_signature aus DB-Record - Gemini-Modelle dynamisch laden statt hardcodiert - Tool-Call-Akkumulator nutzt id als primären Key (Gemini sendet parallele Calls ohne index) - content=null → content='' für Gemini-Kompatibilität
This commit is contained in:
1 parent
e87455ffe9
commit
145441fa7b
6 files changed
+149
-65
No files matched your search
+16
-18
@@ -1566,29 +1566,27 @@ Alle History-Queries (initial load, before/after-Paging, Polling) sortieren nach
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### deploy.sh
|
**WICHTIG:** Das Deployment ist ein **manueller Vorgang**, der durch den Benutzer auf dem Server durchgeführt werden muss. Das System führt kein automatisches Deployment durch.
|
||||||
|
|
||||||
```bash
|
### Goldene Regel für Änderungen
|
||||||
#!/bin/bash
|
* **Arbeiten Sie AUSSCHLIESSLICH im Verzeichnis `wwwroot/`** für Frontend-Änderungen.
|
||||||
set -e
|
* **Editiere NIEMALS Dateien in `Build/*/publish/wwwroot/`!** Diese Verzeichnisse werden bei jedem Build überschrieben.
|
||||||
cd /app/gott
|
* Das Verzeichnis `Build/*/publish/data/gott.db` darf niemals manuell gelöscht, geleert oder direkt modifiziert werden.
|
||||||
export PATH="$HOME/.dotnet:$PATH"
|
|
||||||
systemctl stop gott
|
|
||||||
dotnet publish -c Release -o Build/Linux/publish
|
|
||||||
systemctl start gott
|
|
||||||
echo "OK"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verzeichnisse
|
### Deployment-Vorgang (Beispiel für Linux)
|
||||||
|
Das vorhandene Shell-Skript `deploy.sh` dient lediglich als Hilfestellung für den manuellen Prozess:
|
||||||
|
|
||||||
- **Quell-Code**: `/app/gott/` (wwwroot/, Services/, etc.)
|
1. **Frontend-Anpassungen vornehmen** (in `wwwroot/`).
|
||||||
- **Build-Output**: `/app/gott/Build/<OS>/publish/`
|
2. **Manuelles Deployment** auf dem Server ausführen:
|
||||||
- **Laufzeit-Daten**: `/app/gott/Build/<OS>/publish/data/` (DB, Agents, Images, Skills)
|
```bash
|
||||||
- **WICHTIG**: Änderungen IMMER in `wwwroot/` machen, NIEMALS in `Build/*/publish/wwwroot/`
|
# Beispiel-Ablauf:
|
||||||
|
systemctl stop gott
|
||||||
|
dotnet publish -c Release -o Build/Linux/publish
|
||||||
|
systemctl start gott
|
||||||
|
```
|
||||||
|
|
||||||
### systemd
|
### systemd
|
||||||
|
Die Anwendung ist als systemd-Service konfiguriert und läuft standardmäßig auf Port 19000.
|
||||||
Die Anwendung läuft als systemd Service (`gott.service`), Kestrel auf Port 19000.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+20
-4
@@ -262,11 +262,27 @@ Verwaltung über **Admin-UI** (`#/server-settings`): Liest `appsettings.Producti
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
```bash
|
**WICHTIG:** Das Deployment ist ein **manueller Vorgang**, der durch den Benutzer auf dem Server durchgeführt werden muss. Das System führt kein automatisches Deployment durch.
|
||||||
./deploy.sh # dotnet publish → systemctl restart gott
|
|
||||||
```
|
|
||||||
|
|
||||||
Läuft als `systemd`-Service, Kestrel auf konfigurierbarem Port (Default 19000), typisch hinter Nginx/Caddy Reverse Proxy.
|
### Goldene Regel für Änderungen
|
||||||
|
* **Arbeiten Sie AUSSCHLIESSLICH im Verzeichnis `wwwroot/`** für Frontend-Änderungen.
|
||||||
|
* **Editiere NIEMALS Dateien in `Build/*/publish/wwwroot/`!** Diese Verzeichnisse werden bei jedem Build überschrieben.
|
||||||
|
* Das Verzeichnis `Build/*/publish/data/gott.db` darf niemals manuell gelöscht, geleert oder direkt modifiziert werden.
|
||||||
|
|
||||||
|
### Deployment-Vorgang (Beispiel für Linux)
|
||||||
|
Das vorhandene Shell-Skript `deploy.sh` dient lediglich als Hilfestellung für den manuellen Prozess:
|
||||||
|
|
||||||
|
1. **Frontend-Anpassungen vornehmen** (in `wwwroot/`).
|
||||||
|
2. **Manuelles Deployment** auf dem Server ausführen:
|
||||||
|
```bash
|
||||||
|
# Beispiel-Ablauf:
|
||||||
|
systemctl stop gott
|
||||||
|
dotnet publish -c Release -o Build/Linux/publish
|
||||||
|
systemctl start gott
|
||||||
|
```
|
||||||
|
|
||||||
|
### systemd
|
||||||
|
Die Anwendung ist als systemd-Service konfiguriert und läuft standardmäßig auf Port 19000.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class MessageRecord
|
|||||||
public int Iteration { get; set; } // welche Loop-Iteration
|
public int Iteration { get; set; } // welche Loop-Iteration
|
||||||
public int AttachmentCount { get; set; } // Anzahl angehängter Dateien
|
public int AttachmentCount { get; set; } // Anzahl angehängter Dateien
|
||||||
public string AttachmentNames { get; set; } // JSON-Array mit Dateinamen
|
public string AttachmentNames { get; set; } // JSON-Array mit Dateinamen
|
||||||
|
public string ThoughtSignature { get; set; } // Gemini thought_signature (nur bei tool_use)
|
||||||
public string Tag { get; set; } // null = normal, "preparer"/"review"/"finalizer" = Sub-Agent-Message
|
public string Tag { get; set; } // null = normal, "preparer"/"review"/"finalizer" = Sub-Agent-Message
|
||||||
public bool IsMarked { get; set; } // als wichtig markiert
|
public bool IsMarked { get; set; } // als wichtig markiert
|
||||||
public string DeletedOn { get; set; } // null = aktiv, sonst Zeitstempel der Löschung
|
public string DeletedOn { get; set; } // null = aktiv, sonst Zeitstempel der Löschung
|
||||||
|
|||||||
@@ -362,22 +362,24 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
try { inputEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
|
try { inputEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
|
||||||
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
|
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
|
||||||
|
|
||||||
|
// thought_signature mitführen falls vorhanden (für Gemini-Provider)
|
||||||
|
object toolUseBlock = !string.IsNullOrEmpty(msg.ThoughtSignature)
|
||||||
|
? (object)new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl, thought_signature = msg.ThoughtSignature }
|
||||||
|
: new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl };
|
||||||
|
|
||||||
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null;
|
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null;
|
||||||
if (lastAssistant is Dictionary<string, object> dict &&
|
if (lastAssistant is Dictionary<string, object> dict &&
|
||||||
dict.TryGetValue("role", out var role) && role.ToString() == "assistant" &&
|
dict.TryGetValue("role", out var role) && role.ToString() == "assistant" &&
|
||||||
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
|
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
|
||||||
{
|
{
|
||||||
contentList.Add(new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl });
|
contentList.Add(toolUseBlock);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
history.Add(new Dictionary<string, object>
|
history.Add(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["role"] = "assistant",
|
["role"] = "assistant",
|
||||||
["content"] = new List<object>
|
["content"] = new List<object> { toolUseBlock }
|
||||||
{
|
|
||||||
new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ public class DatabaseService
|
|||||||
TryAddColumn(conn, "agents", "tool_choice", "TEXT DEFAULT 'any'");
|
TryAddColumn(conn, "agents", "tool_choice", "TEXT DEFAULT 'any'");
|
||||||
TryAddColumn(conn, "agents", "custom_commands", "TEXT");
|
TryAddColumn(conn, "agents", "custom_commands", "TEXT");
|
||||||
TryAddColumn(conn, "messages", "is_marked", "INTEGER NOT NULL DEFAULT 0");
|
TryAddColumn(conn, "messages", "is_marked", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
TryAddColumn(conn, "messages", "thought_signature", "TEXT");
|
||||||
TryAddColumn(conn, "agents", "auto_commit", "INTEGER NOT NULL DEFAULT 0");
|
TryAddColumn(conn, "agents", "auto_commit", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
|
||||||
// Migration: auto_commit von INTEGER (0/1) zu TEXT ("", "mention", "forbid")
|
// Migration: auto_commit von INTEGER (0/1) zu TEXT ("", "mention", "forbid")
|
||||||
@@ -768,8 +769,8 @@ public class DatabaseService
|
|||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
record.CreatedAt = DateTime.UtcNow.ToString("o");
|
record.CreatedAt = DateTime.UtcNow.ToString("o");
|
||||||
return await conn.ExecuteScalarAsync<long>(@"
|
return await conn.ExecuteScalarAsync<long>(@"
|
||||||
INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, tag, created_at)
|
INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, tag, thought_signature, created_at)
|
||||||
VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @Tag, @CreatedAt);
|
VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @Tag, @ThoughtSignature, @CreatedAt);
|
||||||
SELECT last_insert_rowid();
|
SELECT last_insert_rowid();
|
||||||
", record);
|
", record);
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-36
@@ -173,13 +173,12 @@ public class OpenAiService
|
|||||||
_nvidiaModels = await FetchModels("https://g4f.space/api/nvidia/models", "Nvidia");
|
_nvidiaModels = await FetchModels("https://g4f.space/api/nvidia/models", "Nvidia");
|
||||||
_pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations");
|
_pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations");
|
||||||
|
|
||||||
// Gemini: nur kostenlose Free-Tier-Modelle (hardcodiert)
|
// Gemini: dynamisch von der API laden (Bearer-Token erforderlich)
|
||||||
_geminiModels = new List<ProviderModel>
|
var geminiApiKey = Providers["gemini"].ApiKey;
|
||||||
{
|
if (!string.IsNullOrEmpty(geminiApiKey))
|
||||||
new ProviderModel("gemini-2.5-pro", "Gemini 2.5 Pro"),
|
_geminiModels = await FetchModels("https://generativelanguage.googleapis.com/v1beta/openai/models", "Gemini", geminiApiKey);
|
||||||
new ProviderModel("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
else
|
||||||
new ProviderModel("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"),
|
Console.WriteLine("[OpenAiService] GeminiApiKey nicht konfiguriert – Gemini-Modelle nicht geladen.");
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<ProviderModel>> FetchModels(string url, string providerName, string apiKey = "")
|
private async Task<List<ProviderModel>> FetchModels(string url, string providerName, string apiKey = "")
|
||||||
@@ -363,7 +362,8 @@ public class OpenAiService
|
|||||||
|
|
||||||
var fullText = new StringBuilder();
|
var fullText = new StringBuilder();
|
||||||
// Tool-Call Akkumulatoren: id → (name, arguments)
|
// Tool-Call Akkumulatoren: id → (name, arguments)
|
||||||
var toolCallAccumulators = new Dictionary<int, (string Id, string Name, StringBuilder Args)>();
|
var toolCallAccumulators = new Dictionary<int, (string Id, string Name, StringBuilder Args, string ThoughtSignature)>();
|
||||||
|
string thoughtSignature = null;
|
||||||
var textStarted = false;
|
var textStarted = false;
|
||||||
var stopReason = "end_turn";
|
var stopReason = "end_turn";
|
||||||
|
|
||||||
@@ -381,13 +381,15 @@ public class OpenAiService
|
|||||||
catch { continue; }
|
catch { continue; }
|
||||||
|
|
||||||
if (!parsed.TryGetProperty("choices", out var choices)) continue;
|
if (!parsed.TryGetProperty("choices", out var choices)) continue;
|
||||||
|
|
||||||
|
|
||||||
foreach (var choice in choices.EnumerateArray())
|
foreach (var choice in choices.EnumerateArray())
|
||||||
{
|
{
|
||||||
// Finish reason
|
// Finish reason
|
||||||
if (choice.TryGetProperty("finish_reason", out var finishProp) && finishProp.ValueKind != JsonValueKind.Null)
|
if (choice.TryGetProperty("finish_reason", out var finishProp) && finishProp.ValueKind != JsonValueKind.Null)
|
||||||
{
|
{
|
||||||
var fr = finishProp.GetString() ?? "";
|
var fr = (finishProp.GetString() ?? "").ToLowerInvariant();
|
||||||
if (fr == "tool_calls") stopReason = "tool_use";
|
if (fr == "tool_calls" || fr == "function_call") stopReason = "tool_use";
|
||||||
else if (fr == "stop") stopReason = "end_turn";
|
else if (fr == "stop") stopReason = "end_turn";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,29 +411,74 @@ public class OpenAiService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Tool-Call Deltas akkumulieren
|
// Tool-Call Deltas akkumulieren
|
||||||
if (delta.TryGetProperty("tool_calls", out var toolCallsProp))
|
if (delta.TryGetProperty("tool_calls", out var toolCallsProp))
|
||||||
{
|
{
|
||||||
foreach (var tc in toolCallsProp.EnumerateArray())
|
foreach (var tc in toolCallsProp.EnumerateArray())
|
||||||
{
|
{
|
||||||
var idx = tc.TryGetProperty("index", out var idxProp) ? idxProp.GetInt32() : 0;
|
// thought_signature aus tc.extra_content.google.thought_signature (Gemini-spezifisch)
|
||||||
|
if (tc.TryGetProperty("extra_content", out var extraContent)
|
||||||
|
&& extraContent.TryGetProperty("google", out var googleContent)
|
||||||
|
&& googleContent.TryGetProperty("thought_signature", out var tcTsProp)
|
||||||
|
&& tcTsProp.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var ts = tcTsProp.GetString();
|
||||||
|
if (!string.IsNullOrEmpty(ts)) thoughtSignature = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID aus diesem Chunk lesen (kann leer sein bei Folge-Deltas)
|
||||||
|
var chunkId = tc.TryGetProperty("id", out var idProp2) ? (idProp2.GetString() ?? "") : "";
|
||||||
|
|
||||||
|
// Schlüssel bestimmen: zuerst per id, dann per index
|
||||||
|
// → verhindert Kollisionen bei parallelen Tool-Calls ohne index (z.B. Gemini)
|
||||||
|
int idx;
|
||||||
|
if (!string.IsNullOrEmpty(chunkId))
|
||||||
|
{
|
||||||
|
// Prüfen ob diese ID schon einem Index zugeordnet ist
|
||||||
|
idx = -1;
|
||||||
|
foreach (var kv in toolCallAccumulators)
|
||||||
|
{
|
||||||
|
if (kv.Value.Id == chunkId) { idx = kv.Key; break; }
|
||||||
|
}
|
||||||
|
if (idx == -1)
|
||||||
|
{
|
||||||
|
// Neue ID → nächsten freien Index vergeben
|
||||||
|
idx = toolCallAccumulators.Count;
|
||||||
|
while (toolCallAccumulators.ContainsKey(idx)) idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
idx = tc.TryGetProperty("index", out var idxProp) ? idxProp.GetInt32() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!toolCallAccumulators.ContainsKey(idx))
|
if (!toolCallAccumulators.ContainsKey(idx))
|
||||||
toolCallAccumulators[idx] = ("", "", new StringBuilder());
|
toolCallAccumulators[idx] = ("", "", new StringBuilder(), null);
|
||||||
|
|
||||||
var (accId, accName, accArgs) = toolCallAccumulators[idx];
|
var (accId, accName, accArgs, accTs) = toolCallAccumulators[idx];
|
||||||
|
|
||||||
if (tc.TryGetProperty("id", out var idProp))
|
if (!string.IsNullOrEmpty(chunkId))
|
||||||
accId = idProp.GetString() ?? accId;
|
accId = chunkId;
|
||||||
|
|
||||||
if (tc.TryGetProperty("function", out var funcProp))
|
if (tc.TryGetProperty("function", out var funcProp))
|
||||||
{
|
{
|
||||||
if (funcProp.TryGetProperty("name", out var nameProp2))
|
if (funcProp.TryGetProperty("name", out var nameProp2))
|
||||||
accName = nameProp2.GetString() ?? accName;
|
{
|
||||||
|
var n = nameProp2.GetString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(n)) accName = n;
|
||||||
|
}
|
||||||
if (funcProp.TryGetProperty("arguments", out var argsProp))
|
if (funcProp.TryGetProperty("arguments", out var argsProp))
|
||||||
accArgs.Append(argsProp.GetString() ?? "");
|
accArgs.Append(argsProp.GetString() ?? "");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toolCallAccumulators[idx] = (accId, accName, accArgs);
|
// Globalen thoughtSignature-Wert auch in accTs des ersten Slots übernehmen
|
||||||
|
if (!string.IsNullOrEmpty(thoughtSignature) && accTs == null && idx == 0)
|
||||||
|
accTs = thoughtSignature;
|
||||||
|
|
||||||
|
toolCallAccumulators[idx] = (accId, accName, accArgs, accTs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,8 +509,8 @@ public class OpenAiService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keine Tool-Calls → fertig
|
// Keine Tool-Calls → fertig (stopReason als Fallback, primär Count prüfen)
|
||||||
if (toolCallAccumulators.Count == 0 || stopReason != "tool_use")
|
if (toolCallAccumulators.Count == 0)
|
||||||
{
|
{
|
||||||
await FinalizeChain(chain, usage, iteration, stopReason, sw);
|
await FinalizeChain(chain, usage, iteration, stopReason, sw);
|
||||||
await TryWriteSse(response, "done", new
|
await TryWriteSse(response, "done", new
|
||||||
@@ -482,35 +529,51 @@ public class OpenAiService
|
|||||||
// Tool-Calls ausführen
|
// Tool-Calls ausführen
|
||||||
// Assistant-Message mit tool_calls in die History einfügen
|
// Assistant-Message mit tool_calls in die History einfügen
|
||||||
var toolCallsForHistory = new List<object>();
|
var toolCallsForHistory = new List<object>();
|
||||||
foreach (var (_, (tcId, tcName, tcArgs)) in toolCallAccumulators)
|
foreach (var (_, (tcId, tcName, tcArgs, tcTs)) in toolCallAccumulators)
|
||||||
{
|
{
|
||||||
toolCallsForHistory.Add(new
|
// thought_signature pro Tool-Call mitschicken falls vorhanden
|
||||||
|
// Gemini erwartet sie in extra_content.google.thought_signature (nicht in function.thought_signature)
|
||||||
|
if (!string.IsNullOrEmpty(tcTs))
|
||||||
{
|
{
|
||||||
id = tcId,
|
toolCallsForHistory.Add(new
|
||||||
type = "function",
|
{
|
||||||
function = new { name = tcName, arguments = tcArgs.ToString() }
|
id = tcId,
|
||||||
});
|
type = "function",
|
||||||
|
function = new { name = tcName, arguments = tcArgs.ToString() },
|
||||||
|
extra_content = new { google = new { thought_signature = tcTs } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toolCallsForHistory.Add(new
|
||||||
|
{
|
||||||
|
id = tcId,
|
||||||
|
type = "function",
|
||||||
|
function = new { name = tcName, arguments = tcArgs.ToString() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Tool-Use in DB speichern
|
// In DB speichern – thought_signature als eigenes Feld
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
{
|
{
|
||||||
ChainId = chainId, Iteration = iteration,
|
ChainId = chainId, Iteration = iteration,
|
||||||
Role = "assistant", Type = "tool_use",
|
Role = "assistant", Type = "tool_use",
|
||||||
Content = tcArgs.ToString(),
|
Content = tcArgs.ToString(),
|
||||||
ToolCallId = tcId, ToolName = tcName
|
ToolCallId = tcId, ToolName = tcName,
|
||||||
|
ThoughtSignature = tcTs
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
apiMessages.Add(new
|
apiMessages.Add(new
|
||||||
{
|
{
|
||||||
role = "assistant",
|
role = "assistant",
|
||||||
content = finalText,
|
content = string.IsNullOrEmpty(finalText) ? "" : finalText,
|
||||||
tool_calls = toolCallsForHistory
|
tool_calls = toolCallsForHistory
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tool-Results sammeln
|
// Tool-Results sammeln
|
||||||
var toolResults = new List<object>();
|
var toolResults = new List<object>();
|
||||||
foreach (var (_, (tcId, tcName, tcArgs)) in toolCallAccumulators)
|
foreach (var (_, (tcId, tcName, tcArgs, _)) in toolCallAccumulators)
|
||||||
{
|
{
|
||||||
// Tool-Start SSE
|
// Tool-Start SSE
|
||||||
await TryWriteSse(response, "tool_start", new { name = tcName, id = tcId }, sseCt);
|
await TryWriteSse(response, "tool_start", new { name = tcName, id = tcId }, sseCt);
|
||||||
@@ -632,12 +695,15 @@ public class OpenAiService
|
|||||||
var tcInput = block.TryGetProperty("input", out var inputProp)
|
var tcInput = block.TryGetProperty("input", out var inputProp)
|
||||||
? JsonSerializer.Serialize(inputProp)
|
? JsonSerializer.Serialize(inputProp)
|
||||||
: "{}";
|
: "{}";
|
||||||
toolCalls.Add(new
|
// thought_signature als eigenes Feld (vom DB-Record direkt in den Block übernommen)
|
||||||
{
|
var tcTs = block.TryGetProperty("thought_signature", out var tsProp2) && tsProp2.ValueKind == JsonValueKind.String
|
||||||
id = tcId,
|
? tsProp2.GetString() : null;
|
||||||
type = "function",
|
|
||||||
function = new { name = tcName, arguments = tcInput }
|
// Gemini erwartet thought_signature in extra_content.google.thought_signature
|
||||||
});
|
if (!string.IsNullOrEmpty(tcTs))
|
||||||
|
toolCalls.Add(new { id = tcId, type = "function", function = new { name = tcName, arguments = tcInput }, extra_content = new { google = new { thought_signature = tcTs } } });
|
||||||
|
else
|
||||||
|
toolCalls.Add(new { id = tcId, type = "function", function = new { name = tcName, arguments = tcInput } });
|
||||||
}
|
}
|
||||||
else if (blockType == "tool_result")
|
else if (blockType == "tool_result")
|
||||||
{
|
{
|
||||||
@@ -665,7 +731,7 @@ public class OpenAiService
|
|||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
role = "assistant",
|
role = "assistant",
|
||||||
content = string.IsNullOrEmpty(text) ? (object)null : text,
|
content = string.IsNullOrEmpty(text) ? "" : text,
|
||||||
tool_calls = toolCalls
|
tool_calls = toolCalls
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user