Files
gott/Services/ChatService.cs
T
Meleki 0120bb8f73 Telegram: Bot=Session statt User-Level, Chat-Code-Refactoring
- Telegram-Config (Bot-Token, Webhook-Secret, ChatId) von User-Settings
  in sessions-Tabelle verschoben → jede Session hat ihren eigenen Bot
- Link-Code-System komplett entfernt (TelegramLinkRecord gelöscht)
- /start verknüpft automatisch, kein /link mehr nötig
- Alle Endpoints session-basiert: /api/sessions/{id}/telegram/*
- Webhook-URL pro Session: /api/telegram/webhook/{sessionId}

Refactoring: Doppelten Chat-Code eliminiert
- Neuer ChatService: PrepareContext, BuildHistoryFromDb, ResolveModelId
- Neues ChatContext-Model als gemeinsame Datenstruktur
- AnthropicService.ChatNonStreaming() für Telegram (statt eigenem Loop)
- TelegramService um ~180 Zeilen reduziert, kein eigener Claude-Loop mehr
- ChatEndpoint nutzt ChatService.PrepareContext() statt eigener Setup-Logik
- Model-Mapping nur noch an einer Stelle (3x → 1x)

Frontend
- Telegram-Section von Settings-Page in Session-Bearbeitungsseite verschoben
- Webhook-Secret readonly, wird einmalig generiert und persistent gespeichert
- Status wird automatisch geladen, Register/Delete je nach Webhook-Status
- Toast-Notification-System (showToast) in ui.js
- Link-Code-Modal und Telegram-Menüpunkt im Dashboard entfernt
- Telegram-Badge auf Session-Cards (konfiguriert/verbunden)

Bugfix: Bearer statt x-api-key für OAuth-Tokens im TelegramService
2026-03-03 21:46:59 +01:00

235 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Models;
namespace Gott.Services;
public class ChatService
{
private readonly DatabaseService _db;
private readonly ToolService _toolService;
private readonly MemoryService _memoryService;
private readonly SkillService _skillService;
public ChatService(DatabaseService db, ToolService toolService, MemoryService memoryService, SkillService skillService)
{
_db = db;
_toolService = toolService;
_memoryService = memoryService;
_skillService = skillService;
}
public static string ResolveModelId(string model)
{
return model switch
{
"sonnet" => "claude-sonnet-4-6",
"sonnet-4-5" => "claude-sonnet-4-5",
"sonnet-4-6" => "claude-sonnet-4-6",
"opus" => "claude-opus-4-6",
"opus-4-5" => "claude-opus-4-5",
"opus-4-6" => "claude-opus-4-6",
"haiku" => "claude-haiku-4-5",
"haiku-4-5" => "claude-haiku-4-5",
_ => "claude-sonnet-4-6"
};
}
/// <summary>
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
/// </summary>
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100)
{
var session = await _db.GetSession(sessionId);
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
return null;
var agent = await _db.GetAgent(session.AgentId);
if (agent == null)
return null;
var modelId = ResolveModelId(agent.Model);
var markedMessages = await _db.GetMarkedMessages(sessionId);
var agentMemoryDir = _memoryService.GetAgentMemoryDir(agent.AgentId);
var agentSkills = await _db.GetAgentSkills(agent.AgentId);
var user = userId != null ? await _db.GetUser(userId) : null;
var systemPromptText = SystemPromptBuilder.Build(
agent, session, markedMessages, _memoryService, agentMemoryDir,
agentSkills, _skillService, userPersona: user?.GetEffectivePersona());
var workspace = session.Workspace;
if (string.IsNullOrEmpty(workspace) && agentSkills.Any())
workspace = "/tmp";
var toolChoice = agent.ToolChoice ?? "any";
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
var toolDefs = (hasWorkspace || includeMemory)
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)
: Array.Empty<object>();
var toolNames = toolDefs.Length > 0
? JsonSerializer.Serialize(toolDefs.Select(td =>
{
var json = JsonSerializer.Serialize(td);
var el = JsonSerializer.Deserialize<JsonElement>(json);
return el.GetProperty("name").GetString();
}).ToArray())
: null;
var chainId = Guid.NewGuid().ToString("N");
await _db.CreateChain(new ChainRecord
{
ChainId = chainId,
SessionId = sessionId,
AgentId = agent.AgentId,
Model = modelId,
SystemPrompt = systemPromptText,
ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null,
ToolNames = toolNames
});
await _db.UpdateSessionTimestamp(sessionId);
if (!string.IsNullOrEmpty(workspace))
Directory.CreateDirectory(workspace);
var systemPrompt = new List<object>();
if (!string.IsNullOrWhiteSpace(systemPromptText))
systemPrompt.Add(new { type = "text", text = systemPromptText });
return new ChatContext
{
ChainId = chainId,
SessionId = sessionId,
ModelId = modelId,
Workspace = workspace,
AgentId = agent.AgentId,
SystemPromptText = systemPromptText,
SystemPrompt = systemPrompt,
ToolChoice = toolChoice,
UseMemory = agent.UseMemory,
WebSearchVersion = agent.WebSearchVersion,
MaxIterations = maxIterations,
Agent = agent,
Session = session
};
}
/// <summary>
/// Baut die Message-History aus der DB (aktive Chains).
/// </summary>
public async Task<List<object>> BuildHistoryFromDb(string sessionId)
{
var chains = await _db.GetActiveChains(sessionId);
var history = new List<object>();
foreach (var chain in chains)
{
var msgs = await _db.GetChainMessages(chain.ChainId);
var grouped = new Dictionary<int, List<MessageRecord>>();
foreach (var msg in msgs)
{
if (!grouped.ContainsKey(msg.Iteration))
grouped[msg.Iteration] = new List<MessageRecord>();
grouped[msg.Iteration].Add(msg);
}
foreach (var kvp in grouped.OrderBy(x => x.Key))
{
foreach (var msg in kvp.Value)
{
if (msg.Role == "user" && msg.Type == "text")
{
var content = new List<object>();
var attachments = await _db.GetAttachments(msg.Id);
foreach (var att in attachments)
{
if (att.Type == "image")
{
content.Add(new
{
type = "image",
source = new { type = "base64", media_type = att.MediaType, data = att.Data }
});
}
else if (att.Type == "document")
{
content.Add(new
{
type = "document",
source = new { type = "base64", media_type = att.MediaType, data = att.Data }
});
}
}
content.Add(new { type = "text", text = msg.Content ?? "" });
history.Add(new Dictionary<string, object> { ["role"] = "user", ["content"] = content });
}
else if (msg.Role == "assistant" && msg.Type == "text")
{
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object> { new { type = "text", text = msg.Content ?? "" } }
});
}
else if (msg.Role == "assistant" && msg.Type == "tool_use")
{
JsonElement inputEl;
try { inputEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null;
if (lastAssistant is Dictionary<string, object> dict &&
dict.TryGetValue("role", out var role) && role.ToString() == "assistant" &&
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 });
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "assistant",
["content"] = new List<object>
{
new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl }
}
});
}
}
else if (msg.Role == "user" && msg.Type == "tool_result")
{
var lastUser = history.Count > 0 ? history[history.Count - 1] : null;
if (lastUser is Dictionary<string, object> dict &&
dict.TryGetValue("role", out var role) && role.ToString() == "user" &&
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
{
contentList.Add(new { type = "tool_result", tool_use_id = msg.ToolCallId, content = msg.Content ?? "", is_error = msg.IsError });
}
else
{
history.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = new List<object>
{
new { type = "tool_result", tool_use_id = msg.ToolCallId, content = msg.Content ?? "", is_error = msg.IsError }
}
});
}
}
}
}
}
return history;
}
}