Benutzer-Werkzeuge

Webseiten-Werkzeuge


test

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen Revision Vorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
test [2025/12/14 22:19]
jango
test [2025/12/14 22:21] (aktuell)
jango
Zeile 1: Zeile 1:
-Mit Voice Transcription (nur OpenAI)+=====Test===== 
 + 
 +=====Default===== 
 +<code CSharp> 
 +using HtmlAgilityPack; 
 +using System; 
 +using System.Collections.Generic; 
 +using System.Diagnostics; 
 +using System.IO; 
 +using System.Linq; 
 +using System.Net.Http; 
 +using System.Net.Http.Headers; 
 +using System.Text; 
 +using System.Text.Encodings.Web; 
 +using System.Text.Json; 
 +using System.Text.Json.Serialization; 
 +using System.Text.RegularExpressions; 
 +using System.Threading; 
 +using System.Threading.Tasks; 
 +using UglyToad.PdfPig; 
 + 
 +namespace ZARAT; 
 + 
 +public static class OpenAI 
 +
 +    // ========================= 
 +    // Config 
 +    // ========================= 
 +    //private const string DEFAULT_API_BASE = "http://localhost:8080/v1"; // LM Studio / local server 
 +    private const string DEFAULT_API_BASE = "https://api.openai.com/v1"; 
 + 
 +    private const string DEFAULT_SYSTEM_PROMPT = 
 +        "Du bist ein hilfreicher Assistent. Verwende Tools (cmd, powershell) um Informationen zu beschaffen! " + 
 +        "Antworte präzise und verschwende keine unnötigen token. aber code u.ä spare nicht, den schreib immer komplett"; 
 + 
 +    // History / Tool output caps (chars) 
 +    private const int MAX_HISTORY_CHARS = 90000; 
 +    private const int MAX_TOOL_CHARS = 30000; 
 + 
 +    private static readonly JsonSerializerOptions JsonOpts = new() 
 +    { 
 +        PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
 +        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 
 +        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 
 +        WriteIndented = false 
 +    }; 
 + 
 +    private static readonly HttpClient Http = new(new HttpClientHandler 
 +    { 
 +        AllowAutoRedirect = true 
 +    }) 
 +    { 
 +        Timeout = TimeSpan.FromSeconds(300) 
 +    }; 
 + 
 +    // Global reference for save_history 
 +    private static List<ChatMessage>? CHAT_HISTORY_REF; 
 + 
 +    public static async Task Main(string[] args) 
 +    { 
 +        // Args: [apiBase] [apiKey?] 
 +        var apiBase = args.Length >= 1 ? args[0] : DEFAULT_API_BASE; 
 +        var apiKey = args.Length >= 2 ? args[1] : (Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? "sk-dummy"); 
 + 
 +        // Configure HttpClient headers (OpenAI-style) 
 +        Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); 
 + 
 +        // /v1/models 
 +        var modelIds = await TryGetModels(apiBase); 
 +        if (modelIds.Count > 0) 
 +        { 
 +            Console.WriteLine("Verfügbare Modelle:"); 
 +            for (int i = 0; i < modelIds.Count; i++) 
 +                Console.WriteLine($"  {i + 1}) {modelIds[i]}"); 
 +        } 
 +        else 
 +        { 
 +            Console.WriteLine("Keine Modelle gefunden (oder /v1/models nicht verfügbar)."); 
 +        } 
 + 
 +        // Choose model 
 +        string model; 
 +        while (true) 
 +        { 
 +            if (modelIds.Count > 0) 
 +            { 
 +                Console.Write($"\nWähle Modellnummer (1..{modelIds.Count}), oder tippe einen Namen: "); 
 +                var choice = (Console.ReadLine() ?? "").Trim(); 
 +                if (int.TryParse(choice, out int idx) && idx >= 1 && idx <= modelIds.Count) 
 +                { 
 +                    model = modelIds[idx - 1]; 
 +                    break; 
 +                } 
 +                if (!string.IsNullOrWhiteSpace(choice)) 
 +                { 
 +                    model = choice; 
 +                    break; 
 +                } 
 +                Console.WriteLine("Bitte eingeben."); 
 +            } 
 +            else 
 +            { 
 +                Console.Write("Model (Name): "); 
 +                var choice = (Console.ReadLine() ?? "").Trim(); 
 +                if (!string.IsNullOrWhiteSpace(choice)) 
 +                { 
 +                    model = choice; 
 +                    break; 
 +                } 
 +                Console.WriteLine("Bitte eingeben."); 
 +            } 
 +        } 
 + 
 +        Console.WriteLine($"Gewähltes Modell: {model}"); 
 + 
 +        var chatHistory = new List<ChatMessage> 
 +        { 
 +            new ChatMessage { Role = "system", Content = DEFAULT_SYSTEM_PROMPT } 
 +        }; 
 +        CHAT_HISTORY_REF = chatHistory; 
 + 
 +        Console.WriteLine("\nTippe 'exit' oder 'quit' zum Beenden.\n"); 
 + 
 +        while (true) 
 +        { 
 +            Console.Write("\n##########\nFrage: "); 
 +            var frage = (Console.ReadLine() ?? "").Trim(); 
 +            if (string.Equals(frage, "exit", StringComparison.OrdinalIgnoreCase) || 
 +                string.Equals(frage, "quit", StringComparison.OrdinalIgnoreCase)) 
 +            { 
 +                Console.WriteLine("Chat beendet."); 
 +                break; 
 +            } 
 + 
 +            chatHistory.Add(new ChatMessage { Role = "user", Content = frage }); 
 + 
 +            var answer = await ChatWithToolsStreamMulti( 
 +                apiBase: apiBase, 
 +                model: model, 
 +                chatHistory: chatHistory, 
 +                maxRounds: 10, 
 +                requireConfirm: false, 
 +                toolChoice: "auto" 
 +            ); 
 + 
 +            Console.WriteLine("\n"); 
 +            chatHistory.Add(new ChatMessage { Role = "assistant", Content = answer }); 
 +        } 
 +    } 
 + 
 +    // ========================= 
 +    // OpenAI REST: /v1/models 
 +    // ========================= 
 +    private static async Task<List<string>> TryGetModels(string apiBase) 
 +    { 
 +        try 
 +        { 
 +            var url = $"{apiBase.TrimEnd('/')}/models"; 
 +            using var req = new HttpRequestMessage(HttpMethod.Get, url); 
 +            using var resp = await Http.SendAsync(req); 
 +            var body = await resp.Content.ReadAsStringAsync(); 
 + 
 +            if (!resp.IsSuccessStatusCode) return new List<string>(); 
 + 
 +            using var doc = JsonDocument.Parse(body); 
 +            if (!doc.RootElement.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Array) 
 +                return new List<string>(); 
 + 
 +            var ids = new List<string>(); 
 +            foreach (var m in data.EnumerateArray()) 
 +            { 
 +                if (m.TryGetProperty("id", out var idEl) && idEl.ValueKind == JsonValueKind.String) 
 +                { 
 +                    var id = idEl.GetString(); 
 +                    if (!string.IsNullOrWhiteSpace(id)) ids.Add(id!); 
 +                } 
 +            } 
 +            return ids; 
 +        } 
 +        catch 
 +        { 
 +            return new List<string>(); 
 +        } 
 +    } 
 + 
 +    // ========================= 
 +    // Core chat loop (multi-round tools) 
 +    // ========================= 
 +    private static async Task<string> ChatWithToolsStreamMulti( 
 +        string apiBase, 
 +        string model, 
 +        List<ChatMessage> chatHistory, 
 +        int maxRounds, 
 +        bool requireConfirm, 
 +        string toolChoice 
 +    ) 
 +    { 
 +        for (int round = 0; round < maxRounds; round++) 
 +        { 
 +            // prune + sanitize 
 +            chatHistory = SanitizeHistoryForRequest(PruneHistoryFifo(chatHistory, MAX_HISTORY_CHARS)); 
 +            // keep original reference list updated 
 +            if (!ReferenceEquals(CHAT_HISTORY_REF, null) && ReferenceEquals(CHAT_HISTORY_REF, chatHistory) == false) 
 +            { 
 +                CHAT_HISTORY_REF = chatHistory; 
 +            } 
 + 
 +            var toolCallsAcc = new Dictionary<int, ToolCall>(); 
 +            var state = new ThinkFilterState(); 
 + 
 +            // streaming chat completion 
 +            await StreamChatCompletion( 
 +                apiBase: apiBase, 
 +                model: model, 
 +                messages: chatHistory, 
 +                tools: ToolsSchema(), 
 +                toolChoice: toolChoice, 
 +                onDeltaContent: delta => StreamContentFilterThink(delta, state), 
 +                onDeltaToolCalls: deltaToolCalls => MergeToolCalls(toolCallsAcc, deltaToolCalls) 
 +            ); 
 + 
 +            var toolCalls = toolCallsAcc.Count > 0 
 +                ? toolCallsAcc.OrderBy(kv => kv.Key).Select(kv => kv.Value).ToList() 
 +                : new List<ToolCall>(); 
 + 
 +            if (toolCalls.Count == 0) 
 +                return state.Answer.Trim(); 
 + 
 +            // add assistant msg with tool_calls 
 +            chatHistory.Add(new ChatMessage 
 +            { 
 +                Role = "assistant", 
 +                Content = string.IsNullOrWhiteSpace(state.Answer) ? "" : state.Answer.Trim(), 
 +                ToolCalls = toolCalls 
 +            }); 
 + 
 +            foreach (var call in toolCalls) 
 +            { 
 +                var fname = call.Function?.Name ?? ""; 
 +                var argsRaw = call.Function?.Arguments ?? "{}"; 
 +                Console.WriteLine($"\n[TOOL CALL] {fname} {argsRaw}"); 
 + 
 +                bool allowed = true; 
 +                if (requireConfirm) 
 +                { 
 +                    Console.Write("Erlauben? (y/n): "); 
 +                    allowed = string.Equals((Console.ReadLine() ?? "").Trim(), "y", StringComparison.OrdinalIgnoreCase); 
 +                } 
 + 
 +                if (!allowed) 
 +                { 
 +                    chatHistory.Add(new ChatMessage 
 +                    { 
 +                        Role = "tool", 
 +                        ToolCallId = call.Id, 
 +                        Name = fname, 
 +                        Content = $"Tool Call '{fname}' wurde vom Nutzer abgelehnt." 
 +                    }); 
 +                    continue; 
 +                } 
 + 
 +                var result = ExecuteToolCall(fname, argsRaw); 
 +                result = ClipToolOutput(result, MAX_TOOL_CHARS); 
 + 
 +                chatHistory.Add(new ChatMessage 
 +                { 
 +                    Role = "tool", 
 +                    ToolCallId = call.Id, 
 +                    Name = fname, 
 +                    Content = result 
 +                }); 
 +            } 
 +        } 
 + 
 +        return "\n[Abbruch: zu viele Tool-Runden]\n"; 
 +    } 
 + 
 +    // ========================= 
 +    // Streaming: SSE parser 
 +    // ========================= 
 +    private static async Task StreamChatCompletion( 
 +        string apiBase, 
 +        string model, 
 +        List<ChatMessage> messages, 
 +        object tools, 
 +        string toolChoice, 
 +        Action<string> onDeltaContent, 
 +        Action<List<ToolCallDelta>> onDeltaToolCalls 
 +    ) 
 +    { 
 +        var url = $"{apiBase.TrimEnd('/')}/chat/completions"; 
 + 
 +        var payload = new Dictionary<string, object?> 
 +        { 
 +            ["model"] = model, 
 +            ["messages"] = messages, 
 +            ["tools"] = tools, 
 +            ["tool_choice"] = toolChoice, 
 +            ["stream"] = true 
 +        }; 
 + 
 +        var json = JsonSerializer.Serialize(payload, JsonOpts); 
 +        using var req = new HttpRequestMessage(HttpMethod.Post, url); 
 +        req.Content = new StringContent(json, Encoding.UTF8, "application/json"); 
 + 
 +        using var resp = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); 
 +        resp.EnsureSuccessStatusCode(); 
 + 
 +        await using var stream = await resp.Content.ReadAsStreamAsync(); 
 +        using var reader = new StreamReader(stream); 
 + 
 +        while (!reader.EndOfStream) 
 +        { 
 +            var line = await reader.ReadLineAsync(); 
 +            if (line == null) break; 
 + 
 +            if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) 
 +            { 
 +                var data = line.Substring(5).Trim(); 
 +                if (data == "[DONE]") break; 
 +                if (string.IsNullOrWhiteSpace(data)) continue; 
 + 
 +                using var doc = JsonDocument.Parse(data); 
 +                var root = doc.RootElement; 
 + 
 +                if (!root.TryGetProperty("choices", out var choices) || choices.ValueKind != JsonValueKind.Array) 
 +                    continue; 
 + 
 +                var choice0 = choices[0]; 
 +                if (!choice0.TryGetProperty("delta", out var delta) || delta.ValueKind != JsonValueKind.Object) 
 +                    continue; 
 + 
 +                // delta.content 
 +                if (delta.TryGetProperty("content", out var contentEl) && contentEl.ValueKind == JsonValueKind.String) 
 +                { 
 +                    var txt = contentEl.GetString() ?? ""; 
 +                    if (txt.Length > 0) onDeltaContent(txt); 
 +                } 
 + 
 +                // delta.tool_calls 
 +                if (delta.TryGetProperty("tool_calls", out var tcEl) && tcEl.ValueKind == JsonValueKind.Array) 
 +                { 
 +                    var list = new List<ToolCallDelta>(); 
 +                    foreach (var item in tcEl.EnumerateArray()) 
 +                    { 
 +                        var d = new ToolCallDelta(); 
 +                        if (item.TryGetProperty("index", out var idxEl) && idxEl.ValueKind == JsonValueKind.Number) 
 +                            d.Index = idxEl.GetInt32(); 
 +                        if (item.TryGetProperty("id", out var idEl) && idEl.ValueKind == JsonValueKind.String) 
 +                            d.Id = idEl.GetString(); 
 + 
 +                        if (item.TryGetProperty("type", out var typeEl) && typeEl.ValueKind == JsonValueKind.String) 
 +                            d.Type = typeEl.GetString(); 
 + 
 +                        if (item.TryGetProperty("function", out var fnEl) && fnEl.ValueKind == JsonValueKind.Object) 
 +                        { 
 +                            if (fnEl.TryGetProperty("name", out var nameEl) && nameEl.ValueKind == JsonValueKind.String) 
 +                                d.FunctionName = nameEl.GetString(); 
 +                            if (fnEl.TryGetProperty("arguments", out var argEl) && argEl.ValueKind == JsonValueKind.String) 
 +                                d.FunctionArguments = argEl.GetString(); 
 +                        } 
 + 
 +                        list.Add(d); 
 +                    } 
 + 
 +                    if (list.Count > 0) onDeltaToolCalls(list); 
 +                } 
 +            } 
 +        } 
 +    } 
 + 
 +    // ========================= 
 +    // Tool schema 
 +    // ========================= 
 +    private static object ToolsSchema() 
 +    { 
 +        // OpenAI tools schema format 
 +        return new object[] 
 +        { 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "read_pdf_text", 
 +                    description = "Extrahiert Text aus einem PDF (für normale PDFs mit Text-Layer).", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["path"] = new { type = "string" }, 
 +                            ["start_page"] = new { type = "integer", @default = 1 }, 
 +                            ["max_pages"] = new { type = "integer", @default = 5 }, 
 +                            ["max_chars"] = new { type = "integer", @default = 12000 }, 
 +                        }, 
 +                        required = new[] { "path"
 +                    } 
 +                } 
 +            }, 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "list_files", 
 +                    description = "Listet alle Dateien und Ordner in einem angegebenen Verzeichnis.", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["directory"] = new { type = "string", description = "Pfad zum Verzeichnis"
 +                        }, 
 +                        required = new[] { "directory"
 +                    } 
 +                } 
 +            }, 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "read_file", 
 +                    description = "Liest Inhalt einer Datei. Unterstützt zeilen/zeichenbasiertes Teil-Lesen, um Kontext zu sparen.", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["path"] = new { type = "string", description = "Pfad zur Datei" }, 
 +                            ["start_line"] = new { type = "integer", description = "Startzeile (1-basiert)", @default = 1 }, 
 +                            ["max_lines"] = new { type = "integer", description = "Max. Anzahl Zeilen", @default = 400 }, 
 +                            ["tail_lines"] = new { type = "integer", description = "Liest die letzten N Zeilen (überschreibt start_line/max_lines)" }, 
 +                            ["start_char"] = new { type = "integer", description = "Startindex Zeichen (0-basiert)" }, 
 +                            ["max_chars"] = new { type = "integer", description = "Max. Anzahl Zeichen" }, 
 +                        }, 
 +                        required = new[] { "path"
 +                    } 
 +                } 
 +            }, 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "execute_cmd", 
 +                    description = "Führt einen Shell Befehl (cmd) aus und liest die Ausgabe von Stdout.", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["command"] = new { type = "string", description = "Den auszuführenden Befehl"
 +                        }, 
 +                        required = new[] { "command"
 +                    } 
 +                } 
 +            }, 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "execute_powershell", 
 +                    description = "Führt einen Powershell Befehl aus und liest die Ausgabe von Stdout.", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["command"] = new { type = "string", description = "Reiner PS-Code (ohne 'powershell -Command ...')"
 +                        }, 
 +                        required = new[] { "command"
 +                    } 
 +                } 
 +            }, 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "fetch_html", 
 +                    description = "Gibt den sichtbaren Text einer Seite zurück (aus HTML extrahiert).", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["url"] = new { type = "string", description = "http(s)-URL" }, 
 +                            ["max_chars"] = new { type = "integer", description = "Hartes Limit (Zeichen)", @default = 8000 }, 
 +                        }, 
 +                        required = new[] { "url"
 +                    } 
 +                } 
 +            }, 
 +            new { 
 +                type = "function", 
 +                function = new { 
 +                    name = "save_history", 
 +                    description = "Speichert die aktuelle Chat-History in eine JSON-Datei.", 
 +                    parameters = new { 
 +                        type = "object", 
 +                        properties = new Dictionary<string, object> { 
 +                            ["path"] = new { type = "string", description = "Zielpfad, z.B. './chat_history.json'" }, 
 +                            ["pretty"] = new { type = "boolean", description = "JSON hübsch formatieren", @default = true } 
 +                        }, 
 +                        required = new[] { "path"
 +                    } 
 +                } 
 +            } 
 +        }; 
 +    } 
 + 
 +    // ========================= 
 +    // Tool call merge (streaming) 
 +    // ========================= 
 +    private static void MergeToolCalls(Dictionary<int, ToolCall> acc, List<ToolCallDelta> deltas) 
 +    { 
 +        foreach (var tc in deltas) 
 +        { 
 +            int idx = tc.Index ?? 0; 
 +            if (!acc.TryGetValue(idx, out var call)) 
 +            { 
 +                call = new ToolCall 
 +                { 
 +                    Id = tc.Id, 
 +                    Type = tc.Type ?? "function", 
 +                    Function = new ToolFunction 
 +                    { 
 +                        Name = tc.FunctionName, 
 +                        Arguments = "" 
 +                    } 
 +                }; 
 +                acc[idx] = call; 
 +            } 
 + 
 +            if (!string.IsNullOrWhiteSpace(tc.Id)) 
 +                call.Id = tc.Id; 
 + 
 +            if (!string.IsNullOrWhiteSpace(tc.FunctionName)) 
 +                call.Function!.Name = tc.FunctionName; 
 + 
 +            if (!string.IsNullOrWhiteSpace(tc.FunctionArguments)) 
 +                call.Function!.Arguments += tc.FunctionArguments; 
 +        } 
 +    } 
 + 
 +    // ========================= 
 +    // <think> filter (stream) 
 +    // ========================= 
 +    private sealed class ThinkFilterState 
 +    { 
 +        public bool InThink { get; set; } 
 +        public string Buf { get; set; } = ""; 
 +        public string Answer { get; set; } = ""; 
 +    } 
 + 
 +    private static void StreamContentFilterThink(string deltaText, ThinkFilterState state) 
 +    { 
 +        state.Buf += deltaText ?? ""; 
 + 
 +        while (state.Buf.Length > 0) 
 +        { 
 +            if (!state.InThink) 
 +            { 
 +                int start = state.Buf.IndexOf("<think>", StringComparison.OrdinalIgnoreCase); 
 +                if (start < 0) 
 +                { 
 +                    state.Answer += state.Buf; 
 +                    Console.Write(state.Buf); 
 +                    state.Buf = ""; 
 +                    return; 
 +                } 
 + 
 +                var before = state.Buf.Substring(0, start); 
 +                state.Answer += before; 
 +                Console.Write(before); 
 + 
 +                state.Buf = state.Buf.Substring(start + "<think>".Length); 
 +                state.InThink = true; 
 +                Console.Write("\n🤔 "); 
 +            } 
 +            else 
 +            { 
 +                int end = state.Buf.IndexOf("</think>", StringComparison.OrdinalIgnoreCase); 
 +                if (end < 0) 
 +                { 
 +                    Console.Write(state.Buf); 
 +                    state.Buf = ""; 
 +                    return; 
 +                } 
 + 
 +                var chunk = state.Buf.Substring(0, end); 
 +                Console.Write(chunk); 
 + 
 +                state.Buf = state.Buf.Substring(end + "</think>".Length); 
 +                state.InThink = false; 
 +                Console.Write("\n"); 
 +            } 
 +        } 
 +    } 
 + 
 +    // ========================= 
 +    // Tool dispatcher 
 +    // ========================= 
 +    private static string ExecuteToolCall(string fname, string argsRaw) 
 +    { 
 +        JsonDocument? doc = null; 
 +        try 
 +        { 
 +            doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(argsRaw) ? "{}" : argsRaw); 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler: Konnte Tool-Argumente nicht als JSON parsen: {e.Message}\nRAW:\n{argsRaw}"; 
 +        } 
 + 
 +        var root = doc.RootElement; 
 + 
 +        try 
 +        { 
 +            return fname switch 
 +            { 
 +                "list_files" => ListFiles(GetString(root, "directory") ?? ""), 
 +                "read_file" => ReadFile( 
 +                    path: GetString(root, "path") ?? "", 
 +                    startLine: GetInt(root, "start_line") ?? 1, 
 +                    maxLines: GetInt(root, "max_lines") ?? 400, 
 +                    startChar: GetInt(root, "start_char"), 
 +                    maxChars: GetInt(root, "max_chars"), 
 +                    tailLines: GetInt(root, "tail_lines"
 +                ), 
 +                "execute_cmd" => ExecuteCmd(GetString(root, "command") ?? ""), 
 +                "execute_powershell" => ExecutePowerShell(GetString(root, "command") ?? ""), 
 +                "fetch_html" => FetchHtml(GetString(root, "url") ?? "", GetInt(root, "max_chars") ?? 8000), 
 +                "save_history" => SaveHistory(GetString(root, "path") ?? "chat_history.json", GetBool(root, "pretty") ?? true), 
 +                "read_pdf_text" => ReadPdfText( 
 +                    path: GetString(root, "path") ?? "", 
 +                    startPage: GetInt(root, "start_page") ?? 1, 
 +                    maxPages: GetInt(root, "max_pages") ?? 5, 
 +                    maxChars: GetInt(root, "max_chars") ?? 12000 
 +                ), 
 +                _ => $"Unbekannte Funktion: {fname}" 
 +            }; 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Tool-Fehler ({fname}): {e.Message}"; 
 +        } 
 +        finally 
 +        { 
 +            doc.Dispose(); 
 +        } 
 +    } 
 + 
 +    private static string? GetString(JsonElement root, string prop) 
 +        => root.TryGetProperty(prop, out var el) && el.ValueKind == JsonValueKind.String ? el.GetString() : null; 
 + 
 +    private static int? GetInt(JsonElement root, string prop) 
 +        => root.TryGetProperty(prop, out var el) && el.ValueKind == JsonValueKind.Number ? el.GetInt32() : null; 
 + 
 +    private static bool? GetBool(JsonElement root, string prop) 
 +        => root.TryGetProperty(prop, out var el) && (el.ValueKind == JsonValueKind.True || el.ValueKind == JsonValueKind.False) ? el.GetBoolean() : null; 
 + 
 +    // ========================= 
 +    // Tool implementations 
 +    // ========================= 
 +    private static string ListFiles(string directory) 
 +    { 
 +        try 
 +        { 
 +            var entries = Directory.EnumerateFileSystemEntries(directory).ToList(); 
 +            // mimic python: files first then folders (optional) 
 +            var files = entries.Where(File.Exists).Select(Path.GetFileName).Where(n => n != null).ToList(); 
 +            var dirs = entries.Where(Directory.Exists).Select(Path.GetFileName).Where(n => n != null).ToList(); 
 +            return string.Join("\n", files.Concat(dirs)); 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler beim Zugriff auf das Verzeichnis: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    private static string ReadFile( 
 +        string path, 
 +        int startLine = 1, 
 +        int maxLines = 400, 
 +        int? startChar = null, 
 +        int? maxChars = null, 
 +        int? tailLines = null 
 +    ) 
 +    { 
 +        try 
 +        { 
 +            if (startLine < 1) startLine = 1; 
 +            if (maxLines < 1) maxLines = 1; 
 + 
 +            var data = File.ReadAllText(path, Encoding.UTF8); 
 + 
 +            if (startChar.HasValue || maxChars.HasValue) 
 +            { 
 +                int s = Math.Max(0, startChar ?? 0); 
 +                if (s > data.Length) s = data.Length; 
 +                string outStr = maxChars.HasValue 
 +                    ? data.Substring(s, Math.Min(maxChars.Value, data.Length - s)) 
 +                    : data.Substring(s); 
 +                return outStr; 
 +            } 
 + 
 +            // line-based 
 +            var lines = File.ReadAllLines(path, Encoding.UTF8); 
 +            int total = lines.Length; 
 + 
 +            int startIdx; 
 +            string[] sel; 
 + 
 +            if (tailLines.HasValue && tailLines.Value > 0) 
 +            { 
 +                int n = Math.Min(tailLines.Value, total); 
 +                startIdx = Math.Max(0, total - n); 
 +                sel = lines.Skip(startIdx).Take(n).ToArray(); 
 +            } 
 +            else 
 +            { 
 +                startIdx = startLine - 1; 
 +                if (startIdx < 0) startIdx = 0; 
 +                sel = lines.Skip(startIdx).Take(maxLines).ToArray(); 
 +            } 
 + 
 +            var header = 
 +                $"[read_file] path={path}\n"
 +                $"[read_file] total_lines={total} selected_lines={sel.Length} range={startIdx + 1}-{startIdx + sel.Length}\n\n"; 
 + 
 +            return header + string.Join("\n", sel); 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler beim Lesen der Datei: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    private static string ExecuteCmd(string command) 
 +    { 
 +        try 
 +        { 
 +            bool isWindows = OperatingSystem.IsWindows(); 
 +            var psi = new ProcessStartInfo 
 +            { 
 +                FileName = isWindows ? "cmd.exe" : "/bin/bash", 
 +                Arguments = isWindows ? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\"", 
 +                RedirectStandardOutput = true, 
 +                RedirectStandardError = true, 
 +                UseShellExecute = false, 
 +                CreateNoWindow = true 
 +            }; 
 + 
 +            using var p = Process.Start(psi)!; 
 +            var stdout = p.StandardOutput.ReadToEnd(); 
 +            var stderr = p.StandardError.ReadToEnd(); 
 +            p.WaitForExit(); 
 + 
 +            if (p.ExitCode == 0) return stdout; 
 +            return $"Fehler: {(string.IsNullOrWhiteSpace(stderr) ? stdout : stderr)}"; 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    private static string ExecutePowerShell(string script) 
 +    { 
 +        try 
 +        { 
 +            // Prefer pwsh if available; fallback to powershell on Windows 
 +            string exe = OperatingSystem.IsWindows() ? "powershell" : "pwsh"; 
 + 
 +            var psi = new ProcessStartInfo 
 +            { 
 +                FileName = exe, 
 +                Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command " + QuoteForProcess(script), 
 +                RedirectStandardOutput = true, 
 +                RedirectStandardError = true, 
 +                UseShellExecute = false, 
 +                CreateNoWindow = true 
 +            }; 
 + 
 +            using var p = Process.Start(psi)!; 
 +            var stdout = p.StandardOutput.ReadToEnd(); 
 +            var stderr = p.StandardError.ReadToEnd(); 
 +            p.WaitForExit(); 
 + 
 +            if (p.ExitCode == 0) return stdout; 
 +            return $"Fehler: {(string.IsNullOrWhiteSpace(stderr) ? stdout : stderr)}"; 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    private static string QuoteForProcess(string s) 
 +    { 
 +        // simplest robust quote for -Command 
 +        // wrap in double quotes and escape internal quotes with backtick for PowerShell 
 +        var escaped = s.Replace("\"", "`\""); 
 +        return $"\"{escaped}\""; 
 +    } 
 + 
 +    private static string FetchHtml(string url, int maxChars = 8000) 
 +    { 
 +        try 
 +        { 
 +            if (!Regex.IsMatch(url, "^https?://", RegexOptions.IgnoreCase)) 
 +                return "Fehler: Nur http(s)-URLs sind erlaubt."; 
 + 
 +            using var req = new HttpRequestMessage(HttpMethod.Get, url); 
 +            req.Headers.UserAgent.ParseAdd("Mozilla/5.0 (compatible; LLM-Helper/1.2)"); 
 +            using var resp = Http.Send(req); 
 +            resp.EnsureSuccessStatusCode(); 
 + 
 +            var html = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult(); 
 + 
 +            var doc = new HtmlDocument(); 
 +            doc.LoadHtml(html); 
 + 
 +            // remove script/style/noscript/template 
 +            var toRemove = doc.DocumentNode.SelectNodes("//script|//style|//noscript|//template"); 
 +            if (toRemove != null) 
 +            { 
 +                foreach (var n in toRemove) n.Remove(); 
 +            } 
 + 
 +            var text = HtmlEntity.DeEntitize(doc.DocumentNode.InnerText); 
 +            text = Regex.Replace(text, @"\n{3,}", "\n\n").Trim(); 
 + 
 +            if (text.Length > maxChars) 
 +                text = text.Substring(0, maxChars) + "\n…[gekürzt]"; 
 + 
 +            return text; 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler beim Abrufen der URL: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    private static string ReadPdfText(string path, int startPage = 1, int maxPages = 5, int maxChars = 12000) 
 +    { 
 +        try 
 +        { 
 +            using var pdf = PdfDocument.Open(path); 
 +            int n = pdf.NumberOfPages; 
 + 
 +            int sp = Math.Max(1, startPage); 
 +            int ep = Math.Min(n, sp + maxPages - 1); 
 + 
 +            var sb = new StringBuilder(); 
 +            for (int p = sp; p <= ep; p++) 
 +            { 
 +                var page = pdf.GetPage(p); 
 +                var txt = page.Text ?? ""; 
 +                sb.Append($"\n--- Seite {p}/{n} ---\n"); 
 +                sb.Append(txt); 
 +                sb.Append('\n'); 
 +            } 
 + 
 +            var outStr = sb.ToString().Trim(); 
 +            if (outStr.Length > maxChars) 
 +                outStr = outStr.Substring(0, maxChars) + "\n…[gekürzt]"; 
 + 
 +            return $"[read_pdf_text] path={path} pages={sp}-{ep}/{n}\n\n{outStr}"; 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler beim PDF-Lesen: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    private static string SaveHistory(string path, bool pretty = true) 
 +    { 
 +        try 
 +        { 
 +            if (CHAT_HISTORY_REF == null) return "Fehler: CHAT_HISTORY_REF ist nicht gesetzt."; 
 + 
 +            var dir = Path.GetDirectoryName(path); 
 +            if (!string.IsNullOrWhiteSpace(dir)) 
 +                Directory.CreateDirectory(dir); 
 + 
 +            var opts = new JsonSerializerOptions(JsonOpts) { WriteIndented = pretty }; 
 +            var json = JsonSerializer.Serialize(CHAT_HISTORY_REF, opts); 
 +            File.WriteAllText(path, json, Encoding.UTF8); 
 + 
 +            return $"OK: History gespeichert nach '{path}' ({CHAT_HISTORY_REF.Count} messages)."; 
 +        } 
 +        catch (Exception e) 
 +        { 
 +            return $"Fehler beim Speichern der History: {e.Message}"; 
 +        } 
 +    } 
 + 
 +    // ========================= 
 +    // History trimming (FIFO) 
 +    // ========================= 
 +    private static int MsgCostChars(ChatMessage m) 
 +    { 
 +        int cost = m.Content?.Length ?? 0; 
 +        if (m.ToolCalls != null && m.ToolCalls.Count > 0) 
 +        { 
 +            try 
 +            { 
 +                cost += JsonSerializer.Serialize(m.ToolCalls, JsonOpts).Length; 
 +            } 
 +            catch 
 +            { 
 +                cost += 500; 
 +            } 
 +        } 
 +        return cost; 
 +    } 
 + 
 +    private static List<ChatMessage> PruneHistoryFifo(List<ChatMessage> messages, int maxChars) 
 +    { 
 +        if (messages.Count == 0) return messages; 
 + 
 +        var system = messages.Where(m => m.Role == "system").ToList(); 
 +        var rest = messages.Where(m => m.Role != "system").ToList(); 
 + 
 +        int total = system.Sum(MsgCostChars); 
 +        int budget = maxChars; 
 + 
 +        var keptRev = new List<ChatMessage>(); 
 +        foreach (var m in rest.AsEnumerable().Reverse()) 
 +        { 
 +            int c = MsgCostChars(m); 
 +            if (keptRev.Count > 0 && (total + c) > budget) 
 +                break; 
 +            keptRev.Add(m); 
 +            total += c; 
 +        } 
 + 
 +        keptRev.Reverse(); 
 +        var kept = new List<ChatMessage>(); 
 +        kept.AddRange(system); 
 +        kept.AddRange(keptRev); 
 +        return kept; 
 +    } 
 + 
 +    private static string ClipToolOutput(string text, int maxChars) 
 +    { 
 +        if (text == null) return ""; 
 +        if (text.Length <= maxChars) return text; 
 +        return text.Substring(0, maxChars) + "\n…[tool output gekürzt]"; 
 +    } 
 + 
 +    // ========================= 
 +    // sanitize history sequences 
 +    // ========================= 
 +    private static List<ChatMessage> SanitizeHistoryForRequest(List<ChatMessage> messages) 
 +    { 
 +        var outList = new List<ChatMessage>(); 
 +        var pending = new HashSet<string>(); 
 + 
 +        foreach (var m in messages) 
 +        { 
 +            if (m.Role == "assistant"
 +            { 
 +                pending.Clear(); 
 +                if (m.ToolCalls != null) 
 +                { 
 +                    foreach (var tc in m.ToolCalls) 
 +                    { 
 +                        if (!string.IsNullOrWhiteSpace(tc.Id)) 
 +                            pending.Add(tc.Id!); 
 +                    } 
 +                } 
 +                outList.Add(m); 
 +                continue; 
 +            } 
 + 
 +            if (m.Role == "tool"
 +            { 
 +                var tcid = m.ToolCallId; 
 +                if (tcid != null && pending.Contains(tcid)) 
 +                { 
 +                    outList.Add(m); 
 +                    pending.Remove(tcid); 
 +                } 
 +                // else drop invalid tool message 
 +                continue; 
 +            } 
 + 
 +            if (m.Role == "user" || m.Role == "system"
 +                pending.Clear(); 
 + 
 +            outList.Add(m); 
 +        } 
 + 
 +        return outList; 
 +    } 
 + 
 +    // ========================= 
 +    // DTOs (OpenAI chat format) 
 +    // ========================= 
 +    public sealed class ChatMessage 
 +    { 
 +        [JsonPropertyName("role")] 
 +        public string Role { get; set; } = ""; 
 + 
 +        [JsonPropertyName("content")] 
 +        public string? Content { get; set; } 
 + 
 +        // assistant tool_calls 
 +        [JsonPropertyName("tool_calls")] 
 +        public List<ToolCall>? ToolCalls { get; set; } 
 + 
 +        // tool message fields 
 +        [JsonPropertyName("tool_call_id")] 
 +        public string? ToolCallId { get; set; } 
 + 
 +        [JsonPropertyName("name")] 
 +        public string? Name { get; set; } 
 +    } 
 + 
 +    public sealed class ToolCall 
 +    { 
 +        [JsonPropertyName("id")] 
 +        public string? Id { get; set; } 
 + 
 +        [JsonPropertyName("type")] 
 +        public string Type { get; set; } = "function"; 
 + 
 +        [JsonPropertyName("function")] 
 +        public ToolFunction? Function { get; set; } 
 +    } 
 + 
 +    public sealed class ToolFunction 
 +    { 
 +        [JsonPropertyName("name")] 
 +        public string? Name { get; set; } 
 + 
 +        [JsonPropertyName("arguments")] 
 +        public string? Arguments { get; set; } 
 +    } 
 + 
 +    // streaming delta structure (partial) 
 +    private sealed class ToolCallDelta 
 +    { 
 +        public int? Index { get; set; } 
 +        public string? Id { get; set; } 
 +        public string? Type { get; set; } 
 +        public string? FunctionName { get; set; } 
 +        public string? FunctionArguments { get; set; } 
 +    } 
 +
 +</code> 
 + 
 +=====Voice Transcription (nur OpenAI)=====
 <code CSharp> <code CSharp>
 using HtmlAgilityPack; using HtmlAgilityPack;
test.1765747179.txt.gz · Zuletzt geändert: 2025/12/14 22:19 von jango