Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| 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 = " | ||
| + | private const string DEFAULT_API_BASE = " | ||
| + | |||
| + | private const string DEFAULT_SYSTEM_PROMPT = | ||
| + | "Du bist ein hilfreicher Assistent. Verwende Tools (cmd, powershell) um Informationen zu beschaffen! " + | ||
| + | " | ||
| + | |||
| + | // 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< | ||
| + | |||
| + | 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(" | ||
| + | |||
| + | // Configure HttpClient headers (OpenAI-style) | ||
| + | Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(" | ||
| + | |||
| + | // / | ||
| + | var modelIds = await TryGetModels(apiBase); | ||
| + | if (modelIds.Count > 0) | ||
| + | { | ||
| + | Console.WriteLine(" | ||
| + | for (int i = 0; i < modelIds.Count; | ||
| + | Console.WriteLine($" | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | Console.WriteLine(" | ||
| + | } | ||
| + | |||
| + | // Choose model | ||
| + | string model; | ||
| + | while (true) | ||
| + | { | ||
| + | if (modelIds.Count > 0) | ||
| + | { | ||
| + | Console.Write($" | ||
| + | var choice = (Console.ReadLine() ?? "" | ||
| + | if (int.TryParse(choice, | ||
| + | { | ||
| + | model = modelIds[idx - 1]; | ||
| + | break; | ||
| + | } | ||
| + | if (!string.IsNullOrWhiteSpace(choice)) | ||
| + | { | ||
| + | model = choice; | ||
| + | break; | ||
| + | } | ||
| + | Console.WriteLine(" | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | Console.Write(" | ||
| + | var choice = (Console.ReadLine() ?? "" | ||
| + | if (!string.IsNullOrWhiteSpace(choice)) | ||
| + | { | ||
| + | model = choice; | ||
| + | break; | ||
| + | } | ||
| + | Console.WriteLine(" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | Console.WriteLine($" | ||
| + | |||
| + | var chatHistory = new List< | ||
| + | { | ||
| + | new ChatMessage { Role = " | ||
| + | }; | ||
| + | CHAT_HISTORY_REF = chatHistory; | ||
| + | |||
| + | Console.WriteLine(" | ||
| + | |||
| + | while (true) | ||
| + | { | ||
| + | Console.Write(" | ||
| + | var frage = (Console.ReadLine() ?? "" | ||
| + | if (string.Equals(frage, | ||
| + | string.Equals(frage, | ||
| + | { | ||
| + | Console.WriteLine(" | ||
| + | break; | ||
| + | } | ||
| + | |||
| + | chatHistory.Add(new ChatMessage { Role = " | ||
| + | |||
| + | var answer = await ChatWithToolsStreamMulti( | ||
| + | apiBase: apiBase, | ||
| + | model: model, | ||
| + | chatHistory: | ||
| + | maxRounds: 10, | ||
| + | requireConfirm: | ||
| + | toolChoice: " | ||
| + | ); | ||
| + | |||
| + | Console.WriteLine(" | ||
| + | chatHistory.Add(new ChatMessage { Role = " | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // OpenAI REST: / | ||
| + | // ========================= | ||
| + | private static async Task< | ||
| + | { | ||
| + | try | ||
| + | { | ||
| + | var url = $" | ||
| + | using var req = new HttpRequestMessage(HttpMethod.Get, | ||
| + | using var resp = await Http.SendAsync(req); | ||
| + | var body = await resp.Content.ReadAsStringAsync(); | ||
| + | |||
| + | if (!resp.IsSuccessStatusCode) return new List< | ||
| + | |||
| + | using var doc = JsonDocument.Parse(body); | ||
| + | if (!doc.RootElement.TryGetProperty(" | ||
| + | return new List< | ||
| + | |||
| + | var ids = new List< | ||
| + | foreach (var m in data.EnumerateArray()) | ||
| + | { | ||
| + | if (m.TryGetProperty(" | ||
| + | { | ||
| + | var id = idEl.GetString(); | ||
| + | if (!string.IsNullOrWhiteSpace(id)) ids.Add(id!); | ||
| + | } | ||
| + | } | ||
| + | return ids; | ||
| + | } | ||
| + | catch | ||
| + | { | ||
| + | return new List< | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // Core chat loop (multi-round tools) | ||
| + | // ========================= | ||
| + | private static async Task< | ||
| + | string apiBase, | ||
| + | string model, | ||
| + | List< | ||
| + | int maxRounds, | ||
| + | bool requireConfirm, | ||
| + | string toolChoice | ||
| + | ) | ||
| + | { | ||
| + | for (int round = 0; round < maxRounds; round++) | ||
| + | { | ||
| + | // prune + sanitize | ||
| + | chatHistory = SanitizeHistoryForRequest(PruneHistoryFifo(chatHistory, | ||
| + | // keep original reference list updated | ||
| + | if (!ReferenceEquals(CHAT_HISTORY_REF, | ||
| + | { | ||
| + | CHAT_HISTORY_REF = chatHistory; | ||
| + | } | ||
| + | |||
| + | var toolCallsAcc = new Dictionary< | ||
| + | var state = new ThinkFilterState(); | ||
| + | |||
| + | // streaming chat completion | ||
| + | await StreamChatCompletion( | ||
| + | apiBase: apiBase, | ||
| + | model: model, | ||
| + | messages: chatHistory, | ||
| + | tools: ToolsSchema(), | ||
| + | toolChoice: toolChoice, | ||
| + | onDeltaContent: | ||
| + | onDeltaToolCalls: | ||
| + | ); | ||
| + | |||
| + | var toolCalls = toolCallsAcc.Count > 0 | ||
| + | ? toolCallsAcc.OrderBy(kv => kv.Key).Select(kv => kv.Value).ToList() | ||
| + | : new List< | ||
| + | |||
| + | if (toolCalls.Count == 0) | ||
| + | return state.Answer.Trim(); | ||
| + | |||
| + | // add assistant msg with tool_calls | ||
| + | chatHistory.Add(new ChatMessage | ||
| + | { | ||
| + | Role = " | ||
| + | Content = string.IsNullOrWhiteSpace(state.Answer) ? "" | ||
| + | ToolCalls = toolCalls | ||
| + | }); | ||
| + | |||
| + | foreach (var call in toolCalls) | ||
| + | { | ||
| + | var fname = call.Function? | ||
| + | var argsRaw = call.Function? | ||
| + | Console.WriteLine($" | ||
| + | |||
| + | bool allowed = true; | ||
| + | if (requireConfirm) | ||
| + | { | ||
| + | Console.Write(" | ||
| + | allowed = string.Equals((Console.ReadLine() ?? "" | ||
| + | } | ||
| + | |||
| + | if (!allowed) | ||
| + | { | ||
| + | chatHistory.Add(new ChatMessage | ||
| + | { | ||
| + | Role = " | ||
| + | ToolCallId = call.Id, | ||
| + | Name = fname, | ||
| + | Content = $"Tool Call ' | ||
| + | }); | ||
| + | continue; | ||
| + | } | ||
| + | |||
| + | var result = ExecuteToolCall(fname, | ||
| + | result = ClipToolOutput(result, | ||
| + | |||
| + | chatHistory.Add(new ChatMessage | ||
| + | { | ||
| + | Role = " | ||
| + | ToolCallId = call.Id, | ||
| + | Name = fname, | ||
| + | Content = result | ||
| + | }); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | return " | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // Streaming: SSE parser | ||
| + | // ========================= | ||
| + | private static async Task StreamChatCompletion( | ||
| + | string apiBase, | ||
| + | string model, | ||
| + | List< | ||
| + | object tools, | ||
| + | string toolChoice, | ||
| + | Action< | ||
| + | Action< | ||
| + | ) | ||
| + | { | ||
| + | var url = $" | ||
| + | |||
| + | var payload = new Dictionary< | ||
| + | { | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | }; | ||
| + | |||
| + | var json = JsonSerializer.Serialize(payload, | ||
| + | using var req = new HttpRequestMessage(HttpMethod.Post, | ||
| + | req.Content = new StringContent(json, | ||
| + | |||
| + | using var resp = await Http.SendAsync(req, | ||
| + | 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(" | ||
| + | { | ||
| + | var data = line.Substring(5).Trim(); | ||
| + | if (data == " | ||
| + | if (string.IsNullOrWhiteSpace(data)) continue; | ||
| + | |||
| + | using var doc = JsonDocument.Parse(data); | ||
| + | var root = doc.RootElement; | ||
| + | |||
| + | if (!root.TryGetProperty(" | ||
| + | continue; | ||
| + | |||
| + | var choice0 = choices[0]; | ||
| + | if (!choice0.TryGetProperty(" | ||
| + | continue; | ||
| + | |||
| + | // delta.content | ||
| + | if (delta.TryGetProperty(" | ||
| + | { | ||
| + | var txt = contentEl.GetString() ?? ""; | ||
| + | if (txt.Length > 0) onDeltaContent(txt); | ||
| + | } | ||
| + | |||
| + | // delta.tool_calls | ||
| + | if (delta.TryGetProperty(" | ||
| + | { | ||
| + | var list = new List< | ||
| + | foreach (var item in tcEl.EnumerateArray()) | ||
| + | { | ||
| + | var d = new ToolCallDelta(); | ||
| + | if (item.TryGetProperty(" | ||
| + | d.Index = idxEl.GetInt32(); | ||
| + | if (item.TryGetProperty(" | ||
| + | d.Id = idEl.GetString(); | ||
| + | |||
| + | if (item.TryGetProperty(" | ||
| + | d.Type = typeEl.GetString(); | ||
| + | |||
| + | if (item.TryGetProperty(" | ||
| + | { | ||
| + | if (fnEl.TryGetProperty(" | ||
| + | d.FunctionName = nameEl.GetString(); | ||
| + | if (fnEl.TryGetProperty(" | ||
| + | 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 = new { | ||
| + | name = " | ||
| + | description = " | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | }, | ||
| + | new { | ||
| + | type = " | ||
| + | function = new { | ||
| + | name = " | ||
| + | description = " | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | }, | ||
| + | new { | ||
| + | type = " | ||
| + | function = new { | ||
| + | name = " | ||
| + | description = "Liest Inhalt einer Datei. Unterstützt zeilen/ | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | }, | ||
| + | new { | ||
| + | type = " | ||
| + | function = new { | ||
| + | name = " | ||
| + | description = " | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | }, | ||
| + | new { | ||
| + | type = " | ||
| + | function = new { | ||
| + | name = " | ||
| + | description = " | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | }, | ||
| + | new { | ||
| + | type = " | ||
| + | function = new { | ||
| + | name = " | ||
| + | description = "Gibt den sichtbaren Text einer Seite zurück (aus HTML extrahiert).", | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | }, | ||
| + | new { | ||
| + | type = " | ||
| + | function = new { | ||
| + | name = " | ||
| + | description = " | ||
| + | parameters = new { | ||
| + | type = " | ||
| + | properties = new Dictionary< | ||
| + | [" | ||
| + | [" | ||
| + | }, | ||
| + | required = new[] { " | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // Tool call merge (streaming) | ||
| + | // ========================= | ||
| + | private static void MergeToolCalls(Dictionary< | ||
| + | { | ||
| + | foreach (var tc in deltas) | ||
| + | { | ||
| + | int idx = tc.Index ?? 0; | ||
| + | if (!acc.TryGetValue(idx, | ||
| + | { | ||
| + | call = new ToolCall | ||
| + | { | ||
| + | Id = tc.Id, | ||
| + | Type = tc.Type ?? " | ||
| + | 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; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // < | ||
| + | // ========================= | ||
| + | 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("< | ||
| + | if (start < 0) | ||
| + | { | ||
| + | state.Answer += state.Buf; | ||
| + | Console.Write(state.Buf); | ||
| + | state.Buf = ""; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | var before = state.Buf.Substring(0, | ||
| + | state.Answer += before; | ||
| + | Console.Write(before); | ||
| + | |||
| + | state.Buf = state.Buf.Substring(start + "< | ||
| + | state.InThink = true; | ||
| + | Console.Write(" | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | int end = state.Buf.IndexOf("</ | ||
| + | if (end < 0) | ||
| + | { | ||
| + | Console.Write(state.Buf); | ||
| + | state.Buf = ""; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | var chunk = state.Buf.Substring(0, | ||
| + | Console.Write(chunk); | ||
| + | |||
| + | state.Buf = state.Buf.Substring(end + "</ | ||
| + | state.InThink = false; | ||
| + | Console.Write(" | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // Tool dispatcher | ||
| + | // ========================= | ||
| + | private static string ExecuteToolCall(string fname, string argsRaw) | ||
| + | { | ||
| + | JsonDocument? | ||
| + | try | ||
| + | { | ||
| + | doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(argsRaw) ? " | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | |||
| + | var root = doc.RootElement; | ||
| + | |||
| + | try | ||
| + | { | ||
| + | return fname switch | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | path: GetString(root, | ||
| + | startLine: GetInt(root, | ||
| + | maxLines: GetInt(root, | ||
| + | startChar: GetInt(root, | ||
| + | maxChars: GetInt(root, | ||
| + | tailLines: GetInt(root, | ||
| + | ), | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | path: GetString(root, | ||
| + | startPage: GetInt(root, | ||
| + | maxPages: GetInt(root, | ||
| + | maxChars: GetInt(root, | ||
| + | ), | ||
| + | _ => $" | ||
| + | }; | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | finally | ||
| + | { | ||
| + | doc.Dispose(); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private static string? GetString(JsonElement root, string prop) | ||
| + | => root.TryGetProperty(prop, | ||
| + | |||
| + | private static int? GetInt(JsonElement root, string prop) | ||
| + | => root.TryGetProperty(prop, | ||
| + | |||
| + | private static bool? GetBool(JsonElement root, string prop) | ||
| + | => root.TryGetProperty(prop, | ||
| + | |||
| + | // ========================= | ||
| + | // 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(" | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | 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, | ||
| + | |||
| + | 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, | ||
| + | : data.Substring(s); | ||
| + | return outStr; | ||
| + | } | ||
| + | |||
| + | // line-based | ||
| + | var lines = File.ReadAllLines(path, | ||
| + | int total = lines.Length; | ||
| + | |||
| + | int startIdx; | ||
| + | string[] sel; | ||
| + | |||
| + | if (tailLines.HasValue && tailLines.Value > 0) | ||
| + | { | ||
| + | int n = Math.Min(tailLines.Value, | ||
| + | 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 = | ||
| + | $" | ||
| + | $" | ||
| + | |||
| + | return header + string.Join(" | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private static string ExecuteCmd(string command) | ||
| + | { | ||
| + | try | ||
| + | { | ||
| + | bool isWindows = OperatingSystem.IsWindows(); | ||
| + | var psi = new ProcessStartInfo | ||
| + | { | ||
| + | FileName = isWindows ? " | ||
| + | Arguments = isWindows ? $"/c {command}" | ||
| + | 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 $" | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private static string ExecutePowerShell(string script) | ||
| + | { | ||
| + | try | ||
| + | { | ||
| + | // Prefer pwsh if available; fallback to powershell on Windows | ||
| + | string exe = OperatingSystem.IsWindows() ? " | ||
| + | |||
| + | var psi = new ProcessStartInfo | ||
| + | { | ||
| + | FileName = exe, | ||
| + | Arguments = " | ||
| + | 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 $" | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | 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 $" | ||
| + | } | ||
| + | |||
| + | private static string FetchHtml(string url, int maxChars = 8000) | ||
| + | { | ||
| + | try | ||
| + | { | ||
| + | if (!Regex.IsMatch(url, | ||
| + | return " | ||
| + | |||
| + | using var req = new HttpRequestMessage(HttpMethod.Get, | ||
| + | req.Headers.UserAgent.ParseAdd(" | ||
| + | using var resp = Http.Send(req); | ||
| + | resp.EnsureSuccessStatusCode(); | ||
| + | |||
| + | var html = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult(); | ||
| + | |||
| + | var doc = new HtmlDocument(); | ||
| + | doc.LoadHtml(html); | ||
| + | |||
| + | // remove script/ | ||
| + | var toRemove = doc.DocumentNode.SelectNodes("// | ||
| + | if (toRemove != null) | ||
| + | { | ||
| + | foreach (var n in toRemove) n.Remove(); | ||
| + | } | ||
| + | |||
| + | var text = HtmlEntity.DeEntitize(doc.DocumentNode.InnerText); | ||
| + | text = Regex.Replace(text, | ||
| + | |||
| + | if (text.Length > maxChars) | ||
| + | text = text.Substring(0, | ||
| + | |||
| + | return text; | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | 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($" | ||
| + | sb.Append(txt); | ||
| + | sb.Append(' | ||
| + | } | ||
| + | |||
| + | var outStr = sb.ToString().Trim(); | ||
| + | if (outStr.Length > maxChars) | ||
| + | outStr = outStr.Substring(0, | ||
| + | |||
| + | return $" | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private static string SaveHistory(string path, bool pretty = true) | ||
| + | { | ||
| + | try | ||
| + | { | ||
| + | if (CHAT_HISTORY_REF == null) return " | ||
| + | |||
| + | 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, | ||
| + | File.WriteAllText(path, | ||
| + | |||
| + | return $"OK: History gespeichert nach ' | ||
| + | } | ||
| + | catch (Exception e) | ||
| + | { | ||
| + | return $" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // History trimming (FIFO) | ||
| + | // ========================= | ||
| + | private static int MsgCostChars(ChatMessage m) | ||
| + | { | ||
| + | int cost = m.Content? | ||
| + | if (m.ToolCalls != null && m.ToolCalls.Count > 0) | ||
| + | { | ||
| + | try | ||
| + | { | ||
| + | cost += JsonSerializer.Serialize(m.ToolCalls, | ||
| + | } | ||
| + | catch | ||
| + | { | ||
| + | cost += 500; | ||
| + | } | ||
| + | } | ||
| + | return cost; | ||
| + | } | ||
| + | |||
| + | private static List< | ||
| + | { | ||
| + | if (messages.Count == 0) return messages; | ||
| + | |||
| + | var system = messages.Where(m => m.Role == " | ||
| + | var rest = messages.Where(m => m.Role != " | ||
| + | |||
| + | int total = system.Sum(MsgCostChars); | ||
| + | int budget = maxChars; | ||
| + | |||
| + | var keptRev = new List< | ||
| + | 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< | ||
| + | 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, | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // sanitize history sequences | ||
| + | // ========================= | ||
| + | private static List< | ||
| + | { | ||
| + | var outList = new List< | ||
| + | var pending = new HashSet< | ||
| + | |||
| + | foreach (var m in messages) | ||
| + | { | ||
| + | if (m.Role == " | ||
| + | { | ||
| + | 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 == " | ||
| + | { | ||
| + | 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 == " | ||
| + | pending.Clear(); | ||
| + | |||
| + | outList.Add(m); | ||
| + | } | ||
| + | |||
| + | return outList; | ||
| + | } | ||
| + | |||
| + | // ========================= | ||
| + | // DTOs (OpenAI chat format) | ||
| + | // ========================= | ||
| + | public sealed class ChatMessage | ||
| + | { | ||
| + | [JsonPropertyName(" | ||
| + | public string Role { get; set; } = ""; | ||
| + | |||
| + | [JsonPropertyName(" | ||
| + | public string? Content { get; set; } | ||
| + | |||
| + | // assistant tool_calls | ||
| + | [JsonPropertyName(" | ||
| + | public List< | ||
| + | |||
| + | // tool message fields | ||
| + | [JsonPropertyName(" | ||
| + | public string? ToolCallId { get; set; } | ||
| + | |||
| + | [JsonPropertyName(" | ||
| + | public string? Name { get; set; } | ||
| + | } | ||
| + | |||
| + | public sealed class ToolCall | ||
| + | { | ||
| + | [JsonPropertyName(" | ||
| + | public string? Id { get; set; } | ||
| + | |||
| + | [JsonPropertyName(" | ||
| + | public string Type { get; set; } = " | ||
| + | |||
| + | [JsonPropertyName(" | ||
| + | public ToolFunction? | ||
| + | } | ||
| + | |||
| + | public sealed class ToolFunction | ||
| + | { | ||
| + | [JsonPropertyName(" | ||
| + | public string? Name { get; set; } | ||
| + | |||
| + | [JsonPropertyName(" | ||
| + | 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; } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | =====Voice Transcription (nur OpenAI)===== | ||
| <code CSharp> | <code CSharp> | ||
| using HtmlAgilityPack; | using HtmlAgilityPack; | ||