fix(openai): Auto-Context, Tool-Result-Matching, Payload-Preview, leere Bubbles
- Auto-Context (Embedding-Suche) für OpenAI-Provider aktiviert: BuildAutoContextAsync auf internal static refactored und in StreamChat + ChatNonStreaming integriert - Tool-Results mit Role="tool" (OpenAI-Konvention) werden in BuildHistoryFromDb jetzt korrekt erkannt (vorher nur Role="user"/Anthropic) - Payload-Preview zeigt für OpenAI-Provider echtes API-Format (function-Wrapper, tool_choice:"auto", max_tokens, stream_options, extra_body) - Tool-Input (Argumente) wird im SSE tool_end-Event mitgesendet (fehlte im OpenAI-Pfad) - Leere Bubbles durch Whitespace-only Text nach </think>-Extraktion behoben (Trim in DB-Save, BuildHistoryFromDb und Frontend-Guard) - History-Toggle-Bug: var→const in for-Loop (Block-Scoping für Closures), tool_result-Matching per data-tool-call-id statt blindes letztes Element - Non-Streaming max_tokens für MiniMax auf 32768 + extra_body.reasoning_split Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ecddbe5f36
commit
1fe04025d3
9 files changed
+164
-42
No files matched your search
@@ -23,6 +23,7 @@ public class OpenAiService
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly DatabaseService _db;
|
||||
private readonly ToolService _toolService;
|
||||
private readonly VectorSearchService _vectorSearch;
|
||||
|
||||
// Provider-Keys (statisch für ChatService.ResolveModelId)
|
||||
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai", "minimax" };
|
||||
@@ -36,7 +37,7 @@ public class OpenAiService
|
||||
/// Konvertiert Anthropic-Format Tool-Definitionen (input_schema) in OpenAI-Format (function/parameters).
|
||||
/// Single source of truth: ToolService.GetToolDefinitions()
|
||||
/// </summary>
|
||||
private static List<object> ConvertTo_openAiToolDefs(object[] anthropicTools)
|
||||
public static List<object> ConvertTo_openAiToolDefs(object[] anthropicTools)
|
||||
{
|
||||
var result = new List<object>();
|
||||
foreach (var tool in anthropicTools)
|
||||
@@ -86,11 +87,12 @@ public class OpenAiService
|
||||
|
||||
private readonly ServerSettingsService _settings;
|
||||
|
||||
public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, ServerSettingsService settings, string dataDir)
|
||||
public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, ServerSettingsService settings, string dataDir, VectorSearchService vectorSearch = null)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
_db = db;
|
||||
_toolService = toolService;
|
||||
_vectorSearch = vectorSearch;
|
||||
_settings = settings;
|
||||
_cacheFile = Path.Combine(dataDir, "models_cache.json");
|
||||
Providers = new Dictionary<string, (string BaseUrl, string ApiKey)>
|
||||
@@ -295,6 +297,7 @@ public class OpenAiService
|
||||
string agentId = null,
|
||||
int maxIterations = 10,
|
||||
string apiKeyOverride = null,
|
||||
bool useAutoContext = true,
|
||||
CancellationToken workCt = default,
|
||||
CancellationToken sseCt = default)
|
||||
{
|
||||
@@ -328,6 +331,32 @@ public class OpenAiService
|
||||
|
||||
try
|
||||
{
|
||||
// ── Automatische Kontext-Injektion via Embedding-Suche ──
|
||||
if (useAutoContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var autoCtx = await AnthropicService.BuildAutoContextAsync(_vectorSearch, messages, chainId, agentId);
|
||||
if (autoCtx != null)
|
||||
{
|
||||
// Auto-Context an System-Prompt anhängen
|
||||
systemPromptText = (systemPromptText ?? "") + "\n" + autoCtx;
|
||||
await SseHelper.TryWriteSse(response, "auto_context", new { context = autoCtx }, sseCt);
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
Role = "system",
|
||||
Type = "auto_context",
|
||||
Content = autoCtx
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[AutoContext/OpenAI] Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var apiMessages = BuildApiMessages(systemPromptText, messages);
|
||||
|
||||
// Agentic Loop
|
||||
@@ -786,8 +815,8 @@ public class OpenAiService
|
||||
});
|
||||
}
|
||||
|
||||
// Text in DB speichern
|
||||
var finalText = fullText.ToString();
|
||||
// Text in DB speichern (Whitespace-only nach </think> ignorieren)
|
||||
var finalText = fullText.ToString().Trim();
|
||||
if (!string.IsNullOrEmpty(finalText))
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
@@ -925,6 +954,7 @@ public class OpenAiService
|
||||
{
|
||||
name = tcName,
|
||||
id = tcId,
|
||||
input = tcArgs.ToString(),
|
||||
result = toolResult,
|
||||
isError
|
||||
}, sseCt);
|
||||
@@ -1029,7 +1059,32 @@ public class OpenAiService
|
||||
Model = ctx.ModelId
|
||||
};
|
||||
|
||||
var apiMessages = BuildApiMessages(ctx.SystemPromptText, messages);
|
||||
// ── Automatische Kontext-Injektion via Embedding-Suche ──
|
||||
var systemPromptText = ctx.SystemPromptText;
|
||||
if (ctx.Agent?.UseAutoContext ?? true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var autoCtx = await AnthropicService.BuildAutoContextAsync(_vectorSearch, messages, ctx.ChainId, ctx.AgentId);
|
||||
if (autoCtx != null)
|
||||
{
|
||||
systemPromptText = (systemPromptText ?? "") + "\n" + autoCtx;
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = ctx.ChainId,
|
||||
Role = "system",
|
||||
Type = "auto_context",
|
||||
Content = autoCtx
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[AutoContext/OpenAI-NonStreaming] Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var apiMessages = BuildApiMessages(systemPromptText, messages);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1039,12 +1094,13 @@ public class OpenAiService
|
||||
onIteration?.Invoke(iterations);
|
||||
|
||||
var client = CreateClient(providerConfig.ApiKey);
|
||||
var maxTokens = provider == "minimax" ? 32768 : 16384;
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = modelId,
|
||||
["messages"] = apiMessages,
|
||||
["stream"] = false,
|
||||
["max_tokens"] = 16384,
|
||||
["max_tokens"] = maxTokens,
|
||||
["tools"] = _openAiToolDefs,
|
||||
["tool_choice"] = "auto"
|
||||
};
|
||||
@@ -1052,6 +1108,9 @@ public class OpenAiService
|
||||
if (provider == "nvidia" && IsNvidiaThinkingModel(modelId))
|
||||
payload["chat_template_kwargs"] = new { thinking = true };
|
||||
|
||||
if (provider == "minimax")
|
||||
payload["extra_body"] = new { reasoning_split = true };
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
|
||||
@@ -1364,7 +1423,7 @@ public class OpenAiService
|
||||
/// <summary>
|
||||
/// Konvertiert Anthropic-Format Messages in OpenAI-kompatibles Format.
|
||||
/// </summary>
|
||||
private static List<object> BuildApiMessages(string systemPromptText, List<object> messages)
|
||||
public static List<object> BuildApiMessages(string systemPromptText, List<object> messages)
|
||||
{
|
||||
var apiMessages = new List<object>();
|
||||
if (!string.IsNullOrEmpty(systemPromptText))
|
||||
|
||||
Reference in new issue
Block a user