Add multi-model panel with Fusion mode
Panel agents answer the user prompt in parallel (read-only, non-streaming); their responses are merged into the worker's last user message as a <panel_responses> block, and the session agent synthesizes one final streamed answer. The RunPanel engine is built layer-ready for future MoA. - New session fields: multi_model_mode (off/fusion), panel_agent_ids (CSV) with idempotent column migration - New partial Services/AnthropicService.Panel.cs holding the RunPanel engine - Provider-agnostic members: Anthropic RunNonStreamingToolLoop / OpenAI CompleteText / CLI plan-mode; all forced read-only, persisted with Tag=panel - SSE events: panel_start, panel_member_start, panel_member_done, panel_done - Frontend: multi-agent picker, live panel bubbles, history rendering - Graceful degradation: panel errors fall back to worker-only Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
b23eba32b4
commit
268f11fa6d
20 files changed
+647
-35
No files matched your search
@@ -1087,6 +1087,56 @@ public class OpenAiService
|
||||
/// Non-Streaming Chat für Telegram, Cronjobs und send_to_session.
|
||||
/// Gibt den finalen Antworttext zurück. Unterstützt Tool-Calling (agentic loop).
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Minimaler Single-Shot-Completion-Call: KEINE Tools, KEINE Persistenz, KEINE Chain.
|
||||
/// Für Panel-Member (Fusion) – liefert nur den Antworttext zurück. So leckt nichts
|
||||
/// ungetaggt in die Worker-History und es entstehen keine Seiteneffekte (Schreiben/Chains).
|
||||
/// </summary>
|
||||
public async Task<string> CompleteText(string modelId, string systemPromptText, List<object> messages, string apiToken)
|
||||
{
|
||||
var resolved = ResolveProviderConfig(modelId, apiToken);
|
||||
if (resolved == null)
|
||||
throw new Exception($"Unbekannter Provider: '{ParseModelString(modelId).provider}'");
|
||||
var (provider, resolvedModelId, providerConfig) = resolved.Value;
|
||||
|
||||
var apiMessages = BuildApiMessages(systemPromptText, messages);
|
||||
var client = CreateClient(providerConfig.ApiKey);
|
||||
var maxTokens = provider == "minimax" ? 32768 : 16384;
|
||||
|
||||
var payload = new Dictionary<string, object>
|
||||
{
|
||||
["model"] = resolvedModelId,
|
||||
["messages"] = apiMessages,
|
||||
["stream"] = false,
|
||||
["max_tokens"] = maxTokens
|
||||
};
|
||||
if (provider == "nvidia" && IsNvidiaThinkingModel(resolvedModelId))
|
||||
payload["chat_template_kwargs"] = new { thinking = true };
|
||||
if (provider == "minimax")
|
||||
{
|
||||
payload["temperature"] = 1.0;
|
||||
payload["top_p"] = 0.95;
|
||||
}
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
using var httpResp = await client.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Post, $"{providerConfig.BaseUrl}/chat/completions") { Content = httpContent });
|
||||
var respBody = await httpResp.Content.ReadAsStringAsync();
|
||||
if (!httpResp.IsSuccessStatusCode)
|
||||
throw new Exception($"OpenAI API {(int)httpResp.StatusCode}: {respBody}");
|
||||
|
||||
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
|
||||
if (!resp.TryGetProperty("choices", out var choices) || choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() == 0)
|
||||
return "";
|
||||
var choice = choices[0];
|
||||
if (!choice.TryGetProperty("message", out var message) || message.ValueKind != JsonValueKind.Object)
|
||||
return "";
|
||||
if (message.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String)
|
||||
return contentProp.GetString() ?? "";
|
||||
return "";
|
||||
}
|
||||
|
||||
public async Task<string> ChatNonStreaming(
|
||||
ChatContext ctx,
|
||||
List<object> messages,
|
||||
|
||||
Reference in new issue
Block a user