refactor: Backend deduplizieren – 13 Dateien, -528 Zeilen netto
ChatContext: 3 identische Context-Klassen (PreparerContext, ReviewContext, FinalizerContext) zu einer SubAgentContext-Klasse zusammengefasst. ChatService: 3x40 Zeilen Sub-Agent-Setup durch BuildSubAgentContext() ersetzt. AnthropicService: 5 Hilfsmethoden extrahiert – ExtractWebSearchSources(), TruncateToolResult(), AddToolsToPayload(), ParseServerToolUse(), TrackUsage(). WorkspaceEndpoint: 5 Session/Agent-Endpoint-Paare durch generische Core-Handler mit basePath-Parameter ersetzt. Ungenutzte rmdir-Endpoints entfernt. AgentEndpoint: Doppeltes Agent-DTO-Mapping durch MapAgentList() ersetzt. SessionEndpoint: Doppelte Session/Agent-Auflösung durch ResolvePromptContext() ersetzt. DatabaseService: Toten Code entfernt (SetSessionTelegramChatId, ClearSessionTelegramChatId, GetSessions, GetUserSessions). ConvertDataUrlToWebP und ResizeDataUrlToWebP zu einer Methode zusammengefasst. TelegramEndpoint: Bot-DTO-Mapping durch MapBotResponse() dedupliziert. KreaEndpoint + SkillEndpoint: Thumbnail-Generierung in ImageService.GenerateThumbnail() extrahiert, SixLabors-Imports aus Endpoints entfernt. CronjobEndpoint: 7x ctx.Items[UserId] durch ctx.GetUserId() ersetzt.
This commit is contained in:
1 parent
0097ce69db
commit
dcedbaa074
13 files changed
+425
-953
No files matched your search
+10
-45
@@ -27,57 +27,22 @@ public static class AgentEndpoint
|
|||||||
{
|
{
|
||||||
var userId = ctx.GetUserId();
|
var userId = ctx.GetUserId();
|
||||||
var agents = await db.GetAgents(userId);
|
var agents = await db.GetAgents(userId);
|
||||||
var allSkills = skillService.GetAllSkills();
|
await ctx.Response.WriteAsJsonAsync(await MapAgentList(agents, userId, db, skillService));
|
||||||
var skillMap = allSkills.ToDictionary(s => s.Slug);
|
|
||||||
|
|
||||||
var results = new List<object>();
|
|
||||||
foreach (var a in agents)
|
|
||||||
{
|
|
||||||
var agentSkillSlugs = await db.GetAgentSkills(a.AgentId);
|
|
||||||
var skills = agentSkillSlugs
|
|
||||||
.Where(slug => skillMap.ContainsKey(slug))
|
|
||||||
.Select(slug => new { slug, name = skillMap[slug].Name, description = skillMap[slug].Description })
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
results.Add(new
|
|
||||||
{
|
|
||||||
a.AgentId,
|
|
||||||
a.Name,
|
|
||||||
Avatar = a.AvatarSmall ?? a.Avatar,
|
|
||||||
a.Description,
|
|
||||||
a.Model,
|
|
||||||
a.CustomCommands,
|
|
||||||
a.AutoCommit,
|
|
||||||
a.UseSoul,
|
|
||||||
a.UseIdentity,
|
|
||||||
a.UseUserInfo,
|
|
||||||
a.UseMemory,
|
|
||||||
a.UseWebSearch,
|
|
||||||
a.WebSearchVersion,
|
|
||||||
a.AllowUserChoice,
|
|
||||||
a.TaskInstruction,
|
|
||||||
a.AvatarScale,
|
|
||||||
a.AvatarOffsetX,
|
|
||||||
a.AvatarOffsetY,
|
|
||||||
a.IsPublic,
|
|
||||||
a.IsDeleted,
|
|
||||||
a.CreatedAt,
|
|
||||||
a.UpdatedAt,
|
|
||||||
Skills = skills
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(results);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleListAvailable(HttpContext ctx, DatabaseService db, SkillService skillService)
|
private static async Task HandleListAvailable(HttpContext ctx, DatabaseService db, SkillService skillService)
|
||||||
{
|
{
|
||||||
var userId = ctx.GetUserId();
|
var userId = ctx.GetUserId();
|
||||||
var agents = await db.GetAvailableAgents(userId);
|
var agents = await db.GetAvailableAgents(userId);
|
||||||
var allSkills = skillService.GetAllSkills();
|
await ctx.Response.WriteAsJsonAsync(await MapAgentList(agents, userId, db, skillService, includeOwner: true));
|
||||||
var skillMap = allSkills.ToDictionary(s => s.Slug);
|
}
|
||||||
|
|
||||||
|
private static async Task<List<object>> MapAgentList(
|
||||||
|
IEnumerable<AgentRecord> agents, string userId, DatabaseService db, SkillService skillService, bool includeOwner = false)
|
||||||
|
{
|
||||||
|
var skillMap = skillService.GetAllSkills().ToDictionary(s => s.Slug);
|
||||||
var results = new List<object>();
|
var results = new List<object>();
|
||||||
|
|
||||||
foreach (var a in agents)
|
foreach (var a in agents)
|
||||||
{
|
{
|
||||||
var agentSkillSlugs = await db.GetAgentSkills(a.AgentId);
|
var agentSkillSlugs = await db.GetAgentSkills(a.AgentId);
|
||||||
@@ -110,12 +75,12 @@ public static class AgentEndpoint
|
|||||||
a.IsDeleted,
|
a.IsDeleted,
|
||||||
a.CreatedAt,
|
a.CreatedAt,
|
||||||
a.UpdatedAt,
|
a.UpdatedAt,
|
||||||
IsOwner = a.UserId == userId,
|
IsOwner = includeOwner ? (bool?)(a.UserId == userId) : null,
|
||||||
Skills = skills
|
Skills = skills
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(results);
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
|
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Gott.Models;
|
using Gott.Models;
|
||||||
|
using Gott.Extensions;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -15,14 +16,14 @@ public static class CronjobEndpoint
|
|||||||
{
|
{
|
||||||
app.MapGet("/api/cronjobs", async (HttpContext ctx, DatabaseService db) =>
|
app.MapGet("/api/cronjobs", async (HttpContext ctx, DatabaseService db) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var cronjobs = await db.GetCronjobs(userId);
|
var cronjobs = await db.GetCronjobs(userId);
|
||||||
return Results.Ok(cronjobs);
|
return Results.Ok(cronjobs);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapGet("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
app.MapGet("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var cronjob = await db.GetCronjob(id);
|
var cronjob = await db.GetCronjob(id);
|
||||||
if (cronjob == null || cronjob.UserId != userId) return Results.NotFound();
|
if (cronjob == null || cronjob.UserId != userId) return Results.NotFound();
|
||||||
return Results.Ok(cronjob);
|
return Results.Ok(cronjob);
|
||||||
@@ -30,7 +31,7 @@ public static class CronjobEndpoint
|
|||||||
|
|
||||||
app.MapPost("/api/cronjobs", async (HttpContext ctx, DatabaseService db) =>
|
app.MapPost("/api/cronjobs", async (HttpContext ctx, DatabaseService db) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
|
|
||||||
var name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
var name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||||
@@ -79,7 +80,7 @@ public static class CronjobEndpoint
|
|||||||
|
|
||||||
app.MapPut("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
app.MapPut("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var existing = await db.GetCronjob(id);
|
var existing = await db.GetCronjob(id);
|
||||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ public static class CronjobEndpoint
|
|||||||
|
|
||||||
app.MapDelete("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
app.MapDelete("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var existing = await db.GetCronjob(id);
|
var existing = await db.GetCronjob(id);
|
||||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ public static class CronjobEndpoint
|
|||||||
|
|
||||||
app.MapPost("/api/cronjobs/{id}/toggle", async (string id, HttpContext ctx, DatabaseService db) =>
|
app.MapPost("/api/cronjobs/{id}/toggle", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var existing = await db.GetCronjob(id);
|
var existing = await db.GetCronjob(id);
|
||||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ public static class CronjobEndpoint
|
|||||||
|
|
||||||
app.MapPost("/api/cronjobs/{id}/run", async (string id, HttpContext ctx, DatabaseService db, CronjobService cronjobService) =>
|
app.MapPost("/api/cronjobs/{id}/run", async (string id, HttpContext ctx, DatabaseService db, CronjobService cronjobService) =>
|
||||||
{
|
{
|
||||||
var userId = ctx.Items["UserId"]?.ToString();
|
var userId = ctx.GetUserId();
|
||||||
var existing = await db.GetCronjob(id);
|
var existing = await db.GetCronjob(id);
|
||||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ using Gott.Services;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using SixLabors.ImageSharp.Formats.Webp;
|
|
||||||
|
|
||||||
namespace Gott.Endpoints
|
namespace Gott.Endpoints
|
||||||
{
|
{
|
||||||
@@ -142,33 +139,21 @@ namespace Gott.Endpoints
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache-Verzeichnis: data/cache/images/
|
|
||||||
var imagesDir = Path.GetDirectoryName(originalPath);
|
var imagesDir = Path.GetDirectoryName(originalPath);
|
||||||
var dataDir = Path.GetDirectoryName(imagesDir);
|
var dataDir = Path.GetDirectoryName(imagesDir);
|
||||||
var cacheDir = Path.Combine(dataDir, "cache", "images");
|
var cacheDir = Path.Combine(dataDir, "cache", "images");
|
||||||
Directory.CreateDirectory(cacheDir);
|
|
||||||
|
|
||||||
var thumbFilename = Path.GetFileNameWithoutExtension(filename) + ".webp";
|
var thumbPath = await ImageService.GenerateThumbnail(originalPath, cacheDir);
|
||||||
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
if (thumbPath != null)
|
||||||
|
|
||||||
if (!File.Exists(thumbPath) || File.GetLastWriteTimeUtc(originalPath) > File.GetLastWriteTimeUtc(thumbPath))
|
|
||||||
{
|
{
|
||||||
try
|
res.ContentType = "image/webp";
|
||||||
{
|
res.Headers["Cache-Control"] = "private, max-age=86400";
|
||||||
using var image = await Image.LoadAsync(originalPath);
|
await res.SendFileAsync(thumbPath);
|
||||||
var maxSize = 320;
|
|
||||||
image.Mutate(x => x.Resize(new ResizeOptions
|
|
||||||
{
|
|
||||||
Size = new Size(maxSize, maxSize),
|
|
||||||
Mode = ResizeMode.Max
|
|
||||||
}));
|
|
||||||
var encoder = new WebpEncoder { Quality = 75 };
|
|
||||||
await image.SaveAsync(thumbPath, encoder);
|
|
||||||
}
|
}
|
||||||
catch
|
else
|
||||||
{
|
{
|
||||||
// Fallback: Original ausliefern
|
// Fallback: Original ausliefern
|
||||||
var contentType = ext switch
|
res.ContentType = ext switch
|
||||||
{
|
{
|
||||||
".png" => "image/png",
|
".png" => "image/png",
|
||||||
".jpg" => "image/jpeg",
|
".jpg" => "image/jpeg",
|
||||||
@@ -176,16 +161,9 @@ namespace Gott.Endpoints
|
|||||||
".gif" => "image/gif",
|
".gif" => "image/gif",
|
||||||
_ => "image/webp",
|
_ => "image/webp",
|
||||||
};
|
};
|
||||||
res.ContentType = contentType;
|
|
||||||
res.Headers["Cache-Control"] = "private, max-age=3600";
|
res.Headers["Cache-Control"] = "private, max-age=3600";
|
||||||
await res.SendFileAsync(originalPath);
|
await res.SendFileAsync(originalPath);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.ContentType = "image/webp";
|
|
||||||
res.Headers["Cache-Control"] = "private, max-age=86400";
|
|
||||||
await res.SendFileAsync(thumbPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,28 +198,26 @@ public static class SessionEndpoint
|
|||||||
ctx.Response.StatusCode = 204;
|
ctx.Response.StatusCode = 204;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
/// <summary>
|
||||||
|
/// Löst Session, Agent, Workspace und Prompt-Abhängigkeiten auf.
|
||||||
|
/// Gibt null zurück und setzt StatusCode bei Fehler.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<(SessionRecord session, AgentRecord agent, IEnumerable<MessageRecord> marked,
|
||||||
|
string memDir, IEnumerable<string> skills, UserRecord user, string workspace, bool planMode)?>
|
||||||
|
ResolvePromptContext(HttpContext ctx, DatabaseService db, MemoryService memoryService)
|
||||||
{
|
{
|
||||||
var userId = ctx.GetUserId();
|
var userId = ctx.GetUserId();
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return null; }
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await db.GetSession(sessionId);
|
var session = await db.GetSession(sessionId);
|
||||||
if (session == null || session.UserId != userId)
|
if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return null; }
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 404;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(session.AgentId))
|
if (string.IsNullOrEmpty(session.AgentId))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var agent = await db.GetAgent(session.AgentId);
|
var agent = await db.GetAgent(session.AgentId);
|
||||||
@@ -227,18 +225,29 @@ public static class SessionEndpoint
|
|||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
|
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var markedMessages = await db.GetMarkedMessages(sessionId);
|
var marked = await db.GetMarkedMessages(sessionId);
|
||||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
var memDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
var skills = await db.GetAgentSkills(agent.AgentId);
|
||||||
var user = await db.GetUser(userId);
|
var user = await db.GetUser(userId);
|
||||||
var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
var workspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||||
if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any())
|
if (string.IsNullOrEmpty(workspace) && skills.Any())
|
||||||
resolvedWorkspace = "/tmp";
|
workspace = "/tmp";
|
||||||
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
|
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
|
||||||
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace, planMode: planMode);
|
|
||||||
|
return (session, agent, marked, memDir, skills, user, workspace, planMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||||
|
{
|
||||||
|
var result = await ResolvePromptContext(ctx, db, memoryService);
|
||||||
|
if (result == null) return;
|
||||||
|
var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value;
|
||||||
|
|
||||||
|
var systemPrompt = SystemPromptBuilder.Build(agent, session, marked, memoryService, memDir,
|
||||||
|
skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode);
|
||||||
|
|
||||||
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
||||||
await ctx.Response.WriteAsync(systemPrompt);
|
await ctx.Response.WriteAsync(systemPrompt);
|
||||||
@@ -246,37 +255,12 @@ public static class SessionEndpoint
|
|||||||
|
|
||||||
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||||
{
|
{
|
||||||
var userId = ctx.GetUserId();
|
var result = await ResolvePromptContext(ctx, db, memoryService);
|
||||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
if (result == null) return;
|
||||||
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
|
var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value;
|
||||||
|
|
||||||
var session = await db.GetSession(sessionId);
|
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, marked, memoryService, memDir,
|
||||||
if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return; }
|
skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(session.AgentId))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var agent = await db.GetAgent(session.AgentId);
|
|
||||||
if (agent == null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var markedMessages = await db.GetMarkedMessages(sessionId);
|
|
||||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
|
||||||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
|
||||||
var user = await db.GetUser(userId);
|
|
||||||
var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
|
||||||
if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any())
|
|
||||||
resolvedWorkspace = "/tmp";
|
|
||||||
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
|
|
||||||
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace, planMode: planMode);
|
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-35
@@ -9,9 +9,6 @@ using Gott.Services;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using SixLabors.ImageSharp.Formats.Webp;
|
|
||||||
|
|
||||||
namespace Gott.Endpoints;
|
namespace Gott.Endpoints;
|
||||||
|
|
||||||
@@ -186,44 +183,22 @@ public static class SkillEndpoint
|
|||||||
var originalPath = Path.Combine(skill.Path, "output", filename);
|
var originalPath = Path.Combine(skill.Path, "output", filename);
|
||||||
if (!File.Exists(originalPath)) { ctx.Response.StatusCode = 404; return; }
|
if (!File.Exists(originalPath)) { ctx.Response.StatusCode = 404; return; }
|
||||||
|
|
||||||
// Cache-Verzeichnis: data/cache/skills/{slug}/
|
|
||||||
var dataDir = Path.GetDirectoryName(skillService.SkillsDir);
|
var dataDir = Path.GetDirectoryName(skillService.SkillsDir);
|
||||||
var cacheDir = Path.Combine(dataDir, "cache", "skills", slug);
|
var cacheDir = Path.Combine(dataDir, "cache", "skills", slug);
|
||||||
Directory.CreateDirectory(cacheDir);
|
|
||||||
|
|
||||||
var thumbFilename = Path.GetFileNameWithoutExtension(filename) + ".webp";
|
var thumbPath = await ImageService.GenerateThumbnail(originalPath, cacheDir);
|
||||||
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
if (thumbPath != null)
|
||||||
|
|
||||||
// Thumbnail generieren wenn nicht gecacht oder Original neuer
|
|
||||||
if (!File.Exists(thumbPath) || File.GetLastWriteTimeUtc(originalPath) > File.GetLastWriteTimeUtc(thumbPath))
|
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
using var image = await Image.LoadAsync(originalPath);
|
|
||||||
|
|
||||||
// Max 320px auf der längsten Seite
|
|
||||||
var maxSize = 320;
|
|
||||||
image.Mutate(x => x.Resize(new ResizeOptions
|
|
||||||
{
|
|
||||||
Size = new Size(maxSize, maxSize),
|
|
||||||
Mode = ResizeMode.Max
|
|
||||||
}));
|
|
||||||
|
|
||||||
var encoder = new WebpEncoder { Quality = 75 };
|
|
||||||
await image.SaveAsync(thumbPath, encoder);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Fallback: Original ausliefern wenn Thumbnail-Generierung fehlschlägt
|
|
||||||
ctx.Response.ContentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
|
||||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
|
||||||
await ctx.Response.SendFileAsync(originalPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Response.ContentType = "image/webp";
|
ctx.Response.ContentType = "image/webp";
|
||||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=86400";
|
ctx.Response.Headers["Cache-Control"] = "private, max-age=86400";
|
||||||
await ctx.Response.SendFileAsync(thumbPath);
|
await ctx.Response.SendFileAsync(thumbPath);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: Original ausliefern
|
||||||
|
ctx.Response.ContentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
||||||
|
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
||||||
|
await ctx.Response.SendFileAsync(originalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Gott.Extensions;
|
using Gott.Extensions;
|
||||||
using Gott.Services;
|
using Gott.Services;
|
||||||
|
using Gott.Models;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
@@ -72,15 +73,10 @@ public static class TelegramEndpoint
|
|||||||
|
|
||||||
// ── Bot CRUD ──
|
// ── Bot CRUD ──
|
||||||
|
|
||||||
private static async Task HandleGetBots(HttpRequest req, HttpResponse res, DatabaseService db)
|
private static async Task<object> MapBotResponse(TelegramBotRecord bot, DatabaseService db)
|
||||||
{
|
|
||||||
var userId = req.HttpContext.GetUserId();
|
|
||||||
var bots = await db.GetTelegramBots(userId);
|
|
||||||
var result = new System.Collections.Generic.List<object>();
|
|
||||||
foreach (var bot in bots)
|
|
||||||
{
|
{
|
||||||
var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId);
|
var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId);
|
||||||
result.Add(new
|
return new
|
||||||
{
|
{
|
||||||
botId = bot.BotId,
|
botId = bot.BotId,
|
||||||
name = bot.Name,
|
name = bot.Name,
|
||||||
@@ -90,8 +86,16 @@ public static class TelegramEndpoint
|
|||||||
sessionCount,
|
sessionCount,
|
||||||
createdAt = bot.CreatedAt,
|
createdAt = bot.CreatedAt,
|
||||||
updatedAt = bot.UpdatedAt
|
updatedAt = bot.UpdatedAt
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task HandleGetBots(HttpRequest req, HttpResponse res, DatabaseService db)
|
||||||
|
{
|
||||||
|
var userId = req.HttpContext.GetUserId();
|
||||||
|
var bots = await db.GetTelegramBots(userId);
|
||||||
|
var result = new System.Collections.Generic.List<object>();
|
||||||
|
foreach (var bot in bots)
|
||||||
|
result.Add(await MapBotResponse(bot, db));
|
||||||
await res.WriteAsJsonAsync(result);
|
await res.WriteAsJsonAsync(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,18 +110,7 @@ public static class TelegramEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId);
|
await res.WriteAsJsonAsync(await MapBotResponse(bot, db));
|
||||||
await res.WriteAsJsonAsync(new
|
|
||||||
{
|
|
||||||
botId = bot.BotId,
|
|
||||||
name = bot.Name,
|
|
||||||
botToken = bot.BotToken,
|
|
||||||
webhookSecret = bot.WebhookSecret,
|
|
||||||
isPublic = bot.IsPublic,
|
|
||||||
sessionCount,
|
|
||||||
createdAt = bot.CreatedAt,
|
|
||||||
updatedAt = bot.UpdatedAt
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleCreateBot(HttpRequest req, HttpResponse res, DatabaseService db)
|
private static async Task HandleCreateBot(HttpRequest req, HttpResponse res, DatabaseService db)
|
||||||
|
|||||||
+132
-391
@@ -38,27 +38,40 @@ public static class WorkspaceEndpoint
|
|||||||
|
|
||||||
public static void MapWorkspaceEndpoint(this IEndpointRouteBuilder app)
|
public static void MapWorkspaceEndpoint(this IEndpointRouteBuilder app)
|
||||||
{
|
{
|
||||||
app.MapGet("/api/workspace/tree", HandleTree);
|
// Session-Workspace-Endpunkte
|
||||||
app.MapGet("/api/workspace/file", HandleFile);
|
app.MapGet("/api/workspace/tree", async ctx =>
|
||||||
app.MapPut("/api/workspace/file", HandleFileSave);
|
await HandleTreeCore(ctx, await ResolveSessionBasePath(ctx)));
|
||||||
|
app.MapGet("/api/workspace/file", async ctx =>
|
||||||
|
await HandleFileCore(ctx, await ResolveSessionBasePath(ctx)));
|
||||||
|
app.MapPut("/api/workspace/file", async ctx =>
|
||||||
|
await HandleFileSaveCore(ctx, await ResolveSessionBasePath(ctx), allowCreate: false));
|
||||||
|
app.MapPost("/api/workspace/mkdir", async ctx =>
|
||||||
|
await HandleMkdirCore(ctx, await ResolveSessionBasePath(ctx)));
|
||||||
|
|
||||||
|
// Agent-Memory-Endpunkte (gleiche Logik, anderer basePath)
|
||||||
|
app.MapGet("/api/workspace/agent/tree", async ctx =>
|
||||||
|
await HandleTreeCore(ctx, ResolveAgentBasePath(ctx)));
|
||||||
|
app.MapGet("/api/workspace/agent/file", async ctx =>
|
||||||
|
await HandleFileCore(ctx, ResolveAgentBasePath(ctx)));
|
||||||
|
app.MapPut("/api/workspace/agent/file", async ctx =>
|
||||||
|
await HandleFileSaveCore(ctx, ResolveAgentBasePath(ctx), allowCreate: true));
|
||||||
|
app.MapPost("/api/workspace/agent/mkdir", async ctx =>
|
||||||
|
await HandleMkdirCore(ctx, ResolveAgentBasePath(ctx)));
|
||||||
|
|
||||||
|
// Sonstige
|
||||||
app.MapGet("/api/workspace/browse", HandleBrowse);
|
app.MapGet("/api/workspace/browse", HandleBrowse);
|
||||||
app.MapPost("/api/workspace/browse/mkdir", HandleBrowseMkdir);
|
app.MapPost("/api/workspace/browse/mkdir", HandleBrowseMkdir);
|
||||||
app.MapGet("/api/workspace/agent/tree", HandleAgentTree);
|
|
||||||
app.MapGet("/api/workspace/agent/file", HandleAgentFile);
|
|
||||||
app.MapPut("/api/workspace/agent/file", HandleAgentFileSave);
|
|
||||||
app.MapGet("/api/workspace/raw", HandleRawFile);
|
app.MapGet("/api/workspace/raw", HandleRawFile);
|
||||||
app.MapPost("/api/workspace/mkdir", HandleMkdir);
|
|
||||||
app.MapDelete("/api/workspace/dir", HandleRmdir);
|
|
||||||
app.MapPost("/api/workspace/agent/mkdir", HandleAgentMkdir);
|
|
||||||
app.MapDelete("/api/workspace/agent/dir", HandleAgentRmdir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── BasePath-Auflösung ──────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lädt Session + User und löst den Workspace-Pfad auf (relativ → absolut via DefaultPath).
|
/// Löst den Workspace-Pfad einer Session auf. Gibt null zurück + setzt 400 bei Fehler.
|
||||||
/// Gibt null zurück und setzt StatusCode 400, wenn kein gültiger Workspace vorhanden ist.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<(SessionRecord session, string basePath)?> ResolveSessionWorkspace(HttpContext ctx, string sessionId)
|
private static async Task<string> ResolveSessionBasePath(HttpContext ctx)
|
||||||
{
|
{
|
||||||
|
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||||
var session = await db.GetSession(sessionId ?? "");
|
var session = await db.GetSession(sessionId ?? "");
|
||||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||||
@@ -78,20 +91,40 @@ public static class WorkspaceEndpoint
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (session, Path.GetFullPath(resolved));
|
return Path.GetFullPath(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleTree(HttpContext ctx)
|
/// <summary>
|
||||||
|
/// Löst den Memory-Pfad eines Agents auf. Gibt null zurück + setzt 400 bei Fehler.
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveAgentBasePath(HttpContext ctx)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
var agentId = ctx.Request.Query["agentId"].FirstOrDefault();
|
||||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
if (string.IsNullOrEmpty(agentId))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 400;
|
||||||
|
ctx.Response.WriteAsJsonAsync(new { error = "agentId fehlt" }).Wait();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
var memoryService = ctx.RequestServices.GetRequiredService<Gott.Services.MemoryService>();
|
||||||
if (result == null) return;
|
var basePath = memoryService.GetAgentMemoryDir(agentId);
|
||||||
var (session, basePath) = result.Value;
|
|
||||||
|
if (!Directory.Exists(basePath))
|
||||||
|
Directory.CreateDirectory(basePath);
|
||||||
|
|
||||||
|
return basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generische Handler ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static async Task HandleTreeCore(HttpContext ctx, string basePath)
|
||||||
|
{
|
||||||
|
if (basePath == null) return;
|
||||||
|
|
||||||
|
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
|
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
|
||||||
|
|
||||||
// Sicherheit: nicht aus dem Workspace ausbrechen
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
if (!targetPath.StartsWith(basePath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 403;
|
ctx.Response.StatusCode = 403;
|
||||||
@@ -107,17 +140,14 @@ public static class WorkspaceEndpoint
|
|||||||
|
|
||||||
var entries = new List<object>();
|
var entries = new List<object>();
|
||||||
|
|
||||||
// Ordner
|
|
||||||
foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase))
|
foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var name = Path.GetFileName(dir);
|
var name = Path.GetFileName(dir);
|
||||||
if (IgnoredDirs.Contains(name)) continue;
|
if (IgnoredDirs.Contains(name)) continue;
|
||||||
|
|
||||||
var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/');
|
var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/');
|
||||||
entries.Add(new { name, path = relPath, type = "dir" });
|
entries.Add(new { name, path = relPath, type = "dir" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dateien
|
|
||||||
foreach (var file in Directory.GetFiles(targetPath).OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
|
foreach (var file in Directory.GetFiles(targetPath).OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var name = Path.GetFileName(file);
|
var name = Path.GetFileName(file);
|
||||||
@@ -125,30 +155,24 @@ public static class WorkspaceEndpoint
|
|||||||
var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/');
|
var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/');
|
||||||
var size = new FileInfo(file).Length;
|
var size = new FileInfo(file).Length;
|
||||||
var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name);
|
var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name);
|
||||||
|
|
||||||
entries.Add(new { name, path = relPath, type = "file", ext, size, isText });
|
entries.Add(new { name, path = relPath, type = "file", ext, size, isText });
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(entries);
|
await ctx.Response.WriteAsJsonAsync(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleFile(HttpContext ctx)
|
private static async Task HandleFileCore(HttpContext ctx, string basePath)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
if (basePath == null) return;
|
||||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
|
|
||||||
|
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||||
if (string.IsNullOrEmpty(filePath))
|
if (string.IsNullOrEmpty(filePath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
|
||||||
if (result == null) return;
|
|
||||||
var (session, basePath) = result.Value;
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||||
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
if (!targetPath.StartsWith(basePath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 403;
|
ctx.Response.StatusCode = 403;
|
||||||
@@ -162,8 +186,6 @@ public static class WorkspaceEndpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
var info = new FileInfo(targetPath);
|
var info = new FileInfo(targetPath);
|
||||||
|
|
||||||
// Max 2MB für Textdateien
|
|
||||||
if (info.Length > 2 * 1024 * 1024)
|
if (info.Length > 2 * 1024 * 1024)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 413;
|
ctx.Response.StatusCode = 413;
|
||||||
@@ -184,34 +206,40 @@ public static class WorkspaceEndpoint
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleFileSave(HttpContext ctx)
|
private static async Task HandleFileSaveCore(HttpContext ctx, string basePath, bool allowCreate)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
if (basePath == null) return;
|
||||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
|
|
||||||
|
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||||
if (string.IsNullOrEmpty(filePath))
|
if (string.IsNullOrEmpty(filePath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 400;
|
ctx.Response.StatusCode = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
|
||||||
if (result == null) return;
|
|
||||||
var (session, basePath) = result.Value;
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||||
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
if (!targetPath.StartsWith(basePath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 403;
|
ctx.Response.StatusCode = 403;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allowCreate)
|
||||||
|
{
|
||||||
|
// Agent-Memory: Verzeichnis erstellen falls nötig
|
||||||
|
var dir = Path.GetDirectoryName(targetPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Session-Workspace: Datei muss existieren
|
||||||
if (!File.Exists(targetPath))
|
if (!File.Exists(targetPath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 404;
|
ctx.Response.StatusCode = 404;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using var reader = new StreamReader(ctx.Request.Body);
|
using var reader = new StreamReader(ctx.Request.Body);
|
||||||
var content = await reader.ReadToEndAsync();
|
var content = await reader.ReadToEndAsync();
|
||||||
@@ -230,6 +258,42 @@ public static class WorkspaceEndpoint
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task HandleMkdirCore(HttpContext ctx, string basePath)
|
||||||
|
{
|
||||||
|
if (basePath == null) return;
|
||||||
|
|
||||||
|
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||||
|
if (string.IsNullOrEmpty(dirPath))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 400;
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||||
|
if (!targetPath.StartsWith(basePath))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 403;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(targetPath))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 409;
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(targetPath);
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new
|
||||||
|
{
|
||||||
|
path = Path.GetRelativePath(basePath, targetPath).Replace('\\', '/'),
|
||||||
|
created = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sonstige Endpunkte ──────────────────────────────────────────────
|
||||||
|
|
||||||
private static async Task HandleBrowse(HttpContext ctx)
|
private static async Task HandleBrowse(HttpContext ctx)
|
||||||
{
|
{
|
||||||
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
||||||
@@ -277,7 +341,6 @@ public static class WorkspaceEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent muss existieren
|
|
||||||
var parent = Path.GetDirectoryName(targetPath);
|
var parent = Path.GetDirectoryName(targetPath);
|
||||||
if (string.IsNullOrEmpty(parent) || !Directory.Exists(parent))
|
if (string.IsNullOrEmpty(parent) || !Directory.Exists(parent))
|
||||||
{
|
{
|
||||||
@@ -290,345 +353,6 @@ public static class WorkspaceEndpoint
|
|||||||
await ctx.Response.WriteAsJsonAsync(new { path = targetPath.Replace('\\', '/'), created = true });
|
await ctx.Response.WriteAsJsonAsync(new { path = targetPath.Replace('\\', '/'), created = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsLikelyTextFile(string name)
|
|
||||||
{
|
|
||||||
var lower = name.ToLowerInvariant();
|
|
||||||
return lower is "makefile" or "dockerfile" or "rakefile" or "gemfile"
|
|
||||||
or "procfile" or "vagrantfile" or "license" or "readme"
|
|
||||||
or "changelog" or "authors" or "contributors";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAgentBasePath(HttpContext ctx, out string error)
|
|
||||||
{
|
|
||||||
error = null;
|
|
||||||
var agentId = ctx.Request.Query["agentId"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(agentId))
|
|
||||||
{
|
|
||||||
error = "agentId fehlt";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var memoryService = ctx.RequestServices.GetRequiredService<Gott.Services.MemoryService>();
|
|
||||||
var basePath = memoryService.GetAgentMemoryDir(agentId);
|
|
||||||
|
|
||||||
if (!Directory.Exists(basePath))
|
|
||||||
Directory.CreateDirectory(basePath);
|
|
||||||
|
|
||||||
return basePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleAgentTree(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var basePath = GetAgentBasePath(ctx, out var error);
|
|
||||||
if (basePath == null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
|
|
||||||
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(targetPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 404;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Verzeichnis nicht gefunden" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries = new List<object>();
|
|
||||||
|
|
||||||
foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var name = Path.GetFileName(dir);
|
|
||||||
if (IgnoredDirs.Contains(name)) continue;
|
|
||||||
var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/');
|
|
||||||
entries.Add(new { name, path = relPath, type = "dir" });
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in Directory.GetFiles(targetPath).OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var name = Path.GetFileName(file);
|
|
||||||
var ext = Path.GetExtension(file);
|
|
||||||
var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/');
|
|
||||||
var size = new FileInfo(file).Length;
|
|
||||||
var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name);
|
|
||||||
entries.Add(new { name, path = relPath, type = "file", ext, size, isText });
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleAgentFile(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var basePath = GetAgentBasePath(ctx, out var error);
|
|
||||||
if (basePath == null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(filePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(targetPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 404;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var info = new FileInfo(targetPath);
|
|
||||||
if (info.Length > 2 * 1024 * 1024)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 413;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Datei zu groß (max. 2MB)" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var content = await File.ReadAllTextAsync(targetPath);
|
|
||||||
var lineCount = content.Split('\n').Length;
|
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new
|
|
||||||
{
|
|
||||||
path = filePath,
|
|
||||||
name = info.Name,
|
|
||||||
size = info.Length,
|
|
||||||
lineCount,
|
|
||||||
content
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleAgentFileSave(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var basePath = GetAgentBasePath(ctx, out var error);
|
|
||||||
if (basePath == null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(filePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verzeichnis erstellen falls nötig
|
|
||||||
var dir = Path.GetDirectoryName(targetPath);
|
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
|
|
||||||
using var reader = new StreamReader(ctx.Request.Body);
|
|
||||||
var content = await reader.ReadToEndAsync();
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(targetPath, content);
|
|
||||||
|
|
||||||
var info = new FileInfo(targetPath);
|
|
||||||
var lineCount = content.Split('\n').Length;
|
|
||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new
|
|
||||||
{
|
|
||||||
path = filePath,
|
|
||||||
name = info.Name,
|
|
||||||
size = info.Length,
|
|
||||||
lineCount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleMkdir(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
|
||||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(dirPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
|
||||||
if (result == null) return;
|
|
||||||
var (session, basePath) = result.Value;
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
|
||||||
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(targetPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 409;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, created = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleRmdir(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
|
||||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(dirPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
|
||||||
if (result == null) return;
|
|
||||||
var (session, basePath) = result.Value;
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
|
||||||
|
|
||||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(targetPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 404;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner nicht gefunden" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.GetFileSystemEntries(targetPath).Length > 0)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 409;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner ist nicht leer" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Delete(targetPath);
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, deleted = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleAgentMkdir(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var basePath = GetAgentBasePath(ctx, out var error);
|
|
||||||
if (basePath == null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(dirPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(targetPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 409;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(targetPath);
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, created = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleAgentRmdir(HttpContext ctx)
|
|
||||||
{
|
|
||||||
var basePath = GetAgentBasePath(ctx, out var error);
|
|
||||||
if (basePath == null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(dirPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
|
||||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 403;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(targetPath))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 404;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner nicht gefunden" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.GetFileSystemEntries(targetPath).Length > 0)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 409;
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner ist nicht leer" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Delete(targetPath);
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, deleted = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> MimeTypes = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" },
|
|
||||||
{ ".gif", "image/gif" }, { ".webp", "image/webp" }, { ".svg", "image/svg+xml" },
|
|
||||||
{ ".bmp", "image/bmp" }, { ".ico", "image/x-icon" },
|
|
||||||
{ ".pdf", "application/pdf" },
|
|
||||||
{ ".mp4", "video/mp4" }, { ".webm", "video/webm" },
|
|
||||||
{ ".mp3", "audio/mpeg" }, { ".wav", "audio/wav" }, { ".ogg", "audio/ogg" },
|
|
||||||
};
|
|
||||||
|
|
||||||
private static async Task HandleRawFile(HttpContext ctx)
|
private static async Task HandleRawFile(HttpContext ctx)
|
||||||
{
|
{
|
||||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||||
@@ -640,12 +364,10 @@ public static class WorkspaceEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
var basePath = await ResolveSessionBasePath(ctx);
|
||||||
if (result == null) return;
|
if (basePath == null) return;
|
||||||
var basePath = result.Value.basePath;
|
|
||||||
|
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||||
|
|
||||||
if (!targetPath.StartsWith(basePath))
|
if (!targetPath.StartsWith(basePath))
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 403;
|
ctx.Response.StatusCode = 403;
|
||||||
@@ -658,11 +380,30 @@ public static class WorkspaceEndpoint
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = Path.GetExtension(targetPath);
|
var ext = Path.GetExtension(targetPath).ToLowerInvariant();
|
||||||
var contentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
ctx.Response.ContentType = ext switch
|
||||||
|
{
|
||||||
|
".png" => "image/png",
|
||||||
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
|
".gif" => "image/gif",
|
||||||
|
".webp" => "image/webp",
|
||||||
|
".svg" => "image/svg+xml",
|
||||||
|
".pdf" => "application/pdf",
|
||||||
|
".mp4" => "video/mp4",
|
||||||
|
".webm" => "video/webm",
|
||||||
|
".mp3" => "audio/mpeg",
|
||||||
|
".wav" => "audio/wav",
|
||||||
|
_ => "application/octet-stream"
|
||||||
|
};
|
||||||
|
|
||||||
ctx.Response.ContentType = contentType;
|
|
||||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
|
||||||
await ctx.Response.SendFileAsync(targetPath);
|
await ctx.Response.SendFileAsync(targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsLikelyTextFile(string name)
|
||||||
|
{
|
||||||
|
var lower = name.ToLowerInvariant();
|
||||||
|
return lower is "makefile" or "dockerfile" or "rakefile" or "gemfile"
|
||||||
|
or "procfile" or "vagrantfile" or "license" or "readme"
|
||||||
|
or "changelog" or "authors" or "contributors";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
-28
@@ -22,16 +22,16 @@ public class ChatContext
|
|||||||
public bool PlanMode { get; set; }
|
public bool PlanMode { get; set; }
|
||||||
|
|
||||||
// Preparer-Agent (vor dem Worker)
|
// Preparer-Agent (vor dem Worker)
|
||||||
public PreparerContext Preparer { get; set; }
|
public SubAgentContext Preparer { get; set; }
|
||||||
|
|
||||||
// Review-Agent
|
// Review-Agent
|
||||||
public ReviewContext Review { get; set; }
|
public SubAgentContext Review { get; set; }
|
||||||
|
|
||||||
// Finalizer-Agent
|
// Finalizer-Agent
|
||||||
public FinalizerContext Finalizer { get; set; }
|
public SubAgentContext Finalizer { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PreparerContext
|
public class SubAgentContext
|
||||||
{
|
{
|
||||||
public string ModelId { get; set; }
|
public string ModelId { get; set; }
|
||||||
public List<object> SystemPrompt { get; set; }
|
public List<object> SystemPrompt { get; set; }
|
||||||
@@ -40,31 +40,11 @@ public class PreparerContext
|
|||||||
public bool UseMemory { get; set; }
|
public bool UseMemory { get; set; }
|
||||||
public string WebSearchVersion { get; set; }
|
public string WebSearchVersion { get; set; }
|
||||||
public string ToolChoice { get; set; }
|
public string ToolChoice { get; set; }
|
||||||
|
public AgentRecord Agent { get; set; }
|
||||||
|
|
||||||
|
// Preparer-spezifisch
|
||||||
public bool AllowUserChoice { get; set; }
|
public bool AllowUserChoice { get; set; }
|
||||||
public AgentRecord Agent { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FinalizerContext
|
// Review-spezifisch
|
||||||
{
|
|
||||||
public string ModelId { get; set; }
|
|
||||||
public List<object> SystemPrompt { get; set; }
|
|
||||||
public string AgentId { get; set; }
|
|
||||||
public string Workspace { get; set; }
|
|
||||||
public bool UseMemory { get; set; }
|
|
||||||
public string WebSearchVersion { get; set; }
|
|
||||||
public string ToolChoice { get; set; }
|
|
||||||
public AgentRecord Agent { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ReviewContext
|
|
||||||
{
|
|
||||||
public string ModelId { get; set; }
|
|
||||||
public List<object> SystemPrompt { get; set; }
|
|
||||||
public string AgentId { get; set; }
|
|
||||||
public string Workspace { get; set; }
|
|
||||||
public bool UseMemory { get; set; }
|
|
||||||
public string WebSearchVersion { get; set; }
|
|
||||||
public string ToolChoice { get; set; }
|
|
||||||
public int MaxReviewIterations { get; set; }
|
public int MaxReviewIterations { get; set; }
|
||||||
public AgentRecord Agent { get; set; }
|
|
||||||
}
|
}
|
||||||
+105
-181
@@ -47,8 +47,8 @@ public class AnthropicService
|
|||||||
string webSearchVersion = null,
|
string webSearchVersion = null,
|
||||||
int maxIterations = 100,
|
int maxIterations = 100,
|
||||||
string toolChoice = "any",
|
string toolChoice = "any",
|
||||||
ReviewContext review = null,
|
SubAgentContext review = null,
|
||||||
FinalizerContext finalizer = null,
|
SubAgentContext finalizer = null,
|
||||||
bool allowUserChoice = true,
|
bool allowUserChoice = true,
|
||||||
bool planMode = false,
|
bool planMode = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -85,30 +85,7 @@ public class AnthropicService
|
|||||||
if (systemPrompt != null && systemPrompt.Count > 0)
|
if (systemPrompt != null && systemPrompt.Count > 0)
|
||||||
payload["system"] = systemPrompt;
|
payload["system"] = systemPrompt;
|
||||||
|
|
||||||
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
AddToolsToPayload(payload, workspace, agentId, useMemory, webSearchVersion, toolChoice, iterations, allowUserChoice, planMode);
|
||||||
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
|
||||||
var hasTools = hasWorkspace || includeMemory;
|
|
||||||
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
|
|
||||||
|
|
||||||
if (hasTools || hasWebSearch)
|
|
||||||
{
|
|
||||||
var tools = new List<object>();
|
|
||||||
if (hasTools)
|
|
||||||
{
|
|
||||||
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice);
|
|
||||||
if (planMode)
|
|
||||||
allTools = _toolService.FilterReadOnlyTools(allTools);
|
|
||||||
tools.AddRange(allTools);
|
|
||||||
}
|
|
||||||
if (hasWebSearch)
|
|
||||||
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
|
|
||||||
payload["tools"] = tools;
|
|
||||||
// "any" erzwingt Tool-Nutzung, aber nur in der ersten Iteration,
|
|
||||||
// damit Claude nicht endlos Tools aufruft wenn die Aufgabe erledigt ist.
|
|
||||||
// Im Plan-Modus nie "any" erzwingen – Claude soll frei antworten können.
|
|
||||||
var effectiveChoice = (!planMode && toolChoice == "any" && iterations == 1) ? "any" : "auto";
|
|
||||||
payload["tool_choice"] = new { type = effectiveChoice };
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = CreateClient();
|
var client = CreateClient();
|
||||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
||||||
@@ -171,22 +148,7 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
else if (blockType == "web_search_tool_result")
|
else if (blockType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
// Sources aus dem Ergebnis-Block extrahieren und speichern
|
var sources = ExtractWebSearchSources(el);
|
||||||
var sources = new List<object>();
|
|
||||||
if (el.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
foreach (var item in searchContent.EnumerateArray())
|
|
||||||
{
|
|
||||||
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
|
|
||||||
if (itemType == "web_search_result")
|
|
||||||
{
|
|
||||||
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
|
|
||||||
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
|
|
||||||
if (!string.IsNullOrEmpty(url))
|
|
||||||
sources.Add(new { url, title });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sources.Count > 0)
|
if (sources.Count > 0)
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
@@ -268,20 +230,11 @@ public class AnthropicService
|
|||||||
var toolResults = new List<object>();
|
var toolResults = new List<object>();
|
||||||
foreach (var tr in executedTools)
|
foreach (var tr in executedTools)
|
||||||
{
|
{
|
||||||
// Tool-Results in der History auf 20.000 Zeichen kürzen.
|
|
||||||
// Das vollständige Ergebnis wurde bereits in der DB gespeichert.
|
|
||||||
// 20k Zeichen ≈ ~5k Tokens – verhindert Token-Explosion bei großen
|
|
||||||
// read_file / execute-Outputs die sich über alle weiteren Iterationen schleppen.
|
|
||||||
const int MaxToolResultChars = 20_000;
|
|
||||||
var content = tr.Result.Length > MaxToolResultChars
|
|
||||||
? tr.Result[..MaxToolResultChars] + $"\n\n[... gekürzt, {tr.Result.Length - MaxToolResultChars} weitere Zeichen ...]"
|
|
||||||
: tr.Result;
|
|
||||||
|
|
||||||
toolResults.Add(new
|
toolResults.Add(new
|
||||||
{
|
{
|
||||||
type = "tool_result",
|
type = "tool_result",
|
||||||
tool_use_id = tr.Id,
|
tool_use_id = tr.Id,
|
||||||
content,
|
content = TruncateToolResult(tr.Result),
|
||||||
is_error = tr.IsError
|
is_error = tr.IsError
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -331,7 +284,7 @@ public class AnthropicService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<PreparerResult> RunPreparer(
|
public async Task<PreparerResult> RunPreparer(
|
||||||
HttpResponse response,
|
HttpResponse response,
|
||||||
PreparerContext preparer,
|
SubAgentContext preparer,
|
||||||
List<object> historyMessages,
|
List<object> historyMessages,
|
||||||
string chainId,
|
string chainId,
|
||||||
string workspace,
|
string workspace,
|
||||||
@@ -416,7 +369,7 @@ public class AnthropicService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<ReviewResult> RunReview(
|
private async Task<ReviewResult> RunReview(
|
||||||
HttpResponse response,
|
HttpResponse response,
|
||||||
ReviewContext review,
|
SubAgentContext review,
|
||||||
List<object> workerMessages,
|
List<object> workerMessages,
|
||||||
string chainId,
|
string chainId,
|
||||||
string workspace,
|
string workspace,
|
||||||
@@ -490,7 +443,7 @@ public class AnthropicService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<int> RunFinalizer(
|
private async Task<int> RunFinalizer(
|
||||||
HttpResponse response,
|
HttpResponse response,
|
||||||
FinalizerContext finalizer,
|
SubAgentContext finalizer,
|
||||||
List<object> workerMessages,
|
List<object> workerMessages,
|
||||||
string chainId,
|
string chainId,
|
||||||
string workspace,
|
string workspace,
|
||||||
@@ -686,22 +639,7 @@ public class AnthropicService
|
|||||||
if (systemPrompt != null && systemPrompt.Count > 0)
|
if (systemPrompt != null && systemPrompt.Count > 0)
|
||||||
payload["system"] = systemPrompt;
|
payload["system"] = systemPrompt;
|
||||||
|
|
||||||
var hasWorkspace = !string.IsNullOrEmpty(agentWorkspace);
|
AddToolsToPayload(payload, agentWorkspace, agentId, useMemory, webSearchVersion, agentToolChoice, iterations);
|
||||||
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
|
||||||
var hasTools = hasWorkspace || includeMemory;
|
|
||||||
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
|
|
||||||
|
|
||||||
if (hasTools || hasWebSearch)
|
|
||||||
{
|
|
||||||
var tools = new List<object>();
|
|
||||||
if (hasTools)
|
|
||||||
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: false));
|
|
||||||
if (hasWebSearch)
|
|
||||||
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
|
|
||||||
payload["tools"] = tools;
|
|
||||||
var effectiveChoice = (agentToolChoice == "any" && iterations == 1) ? "any" : "auto";
|
|
||||||
payload["tool_choice"] = new { type = effectiveChoice };
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = CreateClient();
|
var client = CreateClient();
|
||||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
||||||
@@ -722,12 +660,7 @@ public class AnthropicService
|
|||||||
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
|
var resp = JsonSerializer.Deserialize<JsonElement>(respBody);
|
||||||
|
|
||||||
if (resp.TryGetProperty("usage", out var u))
|
if (resp.TryGetProperty("usage", out var u))
|
||||||
{
|
TrackUsage(usage, u);
|
||||||
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
|
|
||||||
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
|
|
||||||
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
|
|
||||||
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
|
|
||||||
}
|
|
||||||
|
|
||||||
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
|
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
|
||||||
var content = resp.GetProperty("content");
|
var content = resp.GetProperty("content");
|
||||||
@@ -795,16 +728,11 @@ public class AnthropicService
|
|||||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, cancellationToken); }
|
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, cancellationToken); }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
const int MaxToolResultChars = 20_000;
|
|
||||||
var truncatedResult = toolResult.Length > MaxToolResultChars
|
|
||||||
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
|
|
||||||
: toolResult;
|
|
||||||
|
|
||||||
toolResults.Add(new
|
toolResults.Add(new
|
||||||
{
|
{
|
||||||
type = "tool_result",
|
type = "tool_result",
|
||||||
tool_use_id = toolId,
|
tool_use_id = toolId,
|
||||||
content = truncatedResult,
|
content = TruncateToolResult(toolResult),
|
||||||
is_error = isError
|
is_error = isError
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -834,23 +762,7 @@ public class AnthropicService
|
|||||||
else if (blockType == "web_search_tool_result")
|
else if (blockType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
||||||
|
var sources = ExtractWebSearchSources(block);
|
||||||
var sources = new List<object>();
|
|
||||||
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
foreach (var item in searchContent.EnumerateArray())
|
|
||||||
{
|
|
||||||
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
|
|
||||||
if (itemType == "web_search_result")
|
|
||||||
{
|
|
||||||
var url = item.TryGetProperty("url", out var u2) ? u2.GetString() ?? "" : "";
|
|
||||||
var title = item.TryGetProperty("title", out var tt2) ? tt2.GetString() ?? "" : "";
|
|
||||||
if (!string.IsNullOrEmpty(url))
|
|
||||||
sources.Add(new { url, title });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sources.Count > 0)
|
if (sources.Count > 0)
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
@@ -866,11 +778,7 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
else if (blockType == "server_tool_use")
|
else if (blockType == "server_tool_use")
|
||||||
{
|
{
|
||||||
var toolId = block.GetProperty("id").GetString() ?? "";
|
var (toolId, toolName, inputEl) = ParseServerToolUse(block);
|
||||||
var toolName = block.GetProperty("name").GetString() ?? "";
|
|
||||||
JsonElement inputEl;
|
|
||||||
try { inputEl = block.GetProperty("input"); }
|
|
||||||
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
|
|
||||||
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
|
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -926,22 +834,7 @@ public class AnthropicService
|
|||||||
if (ctx.SystemPrompt != null && ctx.SystemPrompt.Count > 0)
|
if (ctx.SystemPrompt != null && ctx.SystemPrompt.Count > 0)
|
||||||
payload["system"] = ctx.SystemPrompt;
|
payload["system"] = ctx.SystemPrompt;
|
||||||
|
|
||||||
var hasWorkspace = !string.IsNullOrEmpty(ctx.Workspace);
|
AddToolsToPayload(payload, ctx.Workspace, ctx.AgentId, ctx.UseMemory, ctx.WebSearchVersion, ctx.ToolChoice, iterations);
|
||||||
var includeMemory = ctx.UseMemory && _memoryService != null && !string.IsNullOrEmpty(ctx.AgentId);
|
|
||||||
var hasTools = hasWorkspace || includeMemory;
|
|
||||||
var hasWebSearch = !string.IsNullOrEmpty(ctx.WebSearchVersion);
|
|
||||||
|
|
||||||
if (hasTools || hasWebSearch)
|
|
||||||
{
|
|
||||||
var tools = new List<object>();
|
|
||||||
if (hasTools)
|
|
||||||
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: false));
|
|
||||||
if (hasWebSearch)
|
|
||||||
tools.Add(new { type = ctx.WebSearchVersion, name = "web_search", max_uses = 5 });
|
|
||||||
payload["tools"] = tools;
|
|
||||||
var effectiveChoice = (ctx.ToolChoice == "any" && iterations == 1) ? "any" : "auto";
|
|
||||||
payload["tool_choice"] = new { type = effectiveChoice };
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = CreateClient();
|
var client = CreateClient();
|
||||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
||||||
@@ -970,12 +863,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
// Usage tracken
|
// Usage tracken
|
||||||
if (resp.TryGetProperty("usage", out var u))
|
if (resp.TryGetProperty("usage", out var u))
|
||||||
{
|
TrackUsage(usage, u);
|
||||||
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
|
|
||||||
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
|
|
||||||
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
|
|
||||||
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
|
|
||||||
}
|
|
||||||
|
|
||||||
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
|
var stopReason = resp.TryGetProperty("stop_reason", out var sr) ? sr.GetString() ?? "" : "";
|
||||||
var content = resp.GetProperty("content");
|
var content = resp.GetProperty("content");
|
||||||
@@ -1021,17 +909,11 @@ public class AnthropicService
|
|||||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId); }
|
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId); }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
// Tool-Result in der History kürzen (wie bei StreamChat)
|
|
||||||
const int MaxToolResultChars = 20_000;
|
|
||||||
var truncatedResult = toolResult.Length > MaxToolResultChars
|
|
||||||
? toolResult[..MaxToolResultChars] + $"\n\n[... gekürzt, {toolResult.Length - MaxToolResultChars} weitere Zeichen ...]"
|
|
||||||
: toolResult;
|
|
||||||
|
|
||||||
toolResults.Add(new
|
toolResults.Add(new
|
||||||
{
|
{
|
||||||
type = "tool_result",
|
type = "tool_result",
|
||||||
tool_use_id = toolId,
|
tool_use_id = toolId,
|
||||||
content = truncatedResult,
|
content = TruncateToolResult(toolResult),
|
||||||
is_error = isError
|
is_error = isError
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1047,21 +929,7 @@ public class AnthropicService
|
|||||||
else if (blockType == "web_search_tool_result")
|
else if (blockType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
assistantContent.Add(JsonSerializer.Deserialize<JsonElement>(block.GetRawText()));
|
||||||
var sources = new List<object>();
|
var sources = ExtractWebSearchSources(block);
|
||||||
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
foreach (var item in searchContent.EnumerateArray())
|
|
||||||
{
|
|
||||||
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
|
|
||||||
if (itemType == "web_search_result")
|
|
||||||
{
|
|
||||||
var url = item.TryGetProperty("url", out var u2) ? u2.GetString() ?? "" : "";
|
|
||||||
var title = item.TryGetProperty("title", out var tt2) ? tt2.GetString() ?? "" : "";
|
|
||||||
if (!string.IsNullOrEmpty(url))
|
|
||||||
sources.Add(new { url, title });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sources.Count > 0)
|
if (sources.Count > 0)
|
||||||
{
|
{
|
||||||
await _db.SaveMessage(new MessageRecord
|
await _db.SaveMessage(new MessageRecord
|
||||||
@@ -1074,11 +942,7 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
else if (blockType == "server_tool_use")
|
else if (blockType == "server_tool_use")
|
||||||
{
|
{
|
||||||
var toolId = block.GetProperty("id").GetString() ?? "";
|
var (toolId, toolName, inputEl) = ParseServerToolUse(block);
|
||||||
var toolName = block.GetProperty("name").GetString() ?? "";
|
|
||||||
JsonElement inputEl;
|
|
||||||
try { inputEl = block.GetProperty("input"); }
|
|
||||||
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
|
|
||||||
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
|
assistantContent.Add(new { type = "server_tool_use", id = toolId, name = toolName, input = inputEl });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1186,14 +1050,7 @@ public class AnthropicService
|
|||||||
{
|
{
|
||||||
if (evt.TryGetProperty("message", out var msg) &&
|
if (evt.TryGetProperty("message", out var msg) &&
|
||||||
msg.TryGetProperty("usage", out var u))
|
msg.TryGetProperty("usage", out var u))
|
||||||
{
|
TrackUsage(usage, u);
|
||||||
if (u.TryGetProperty("input_tokens", out var it))
|
|
||||||
usage.InputTokens += it.GetInt64();
|
|
||||||
if (u.TryGetProperty("cache_creation_input_tokens", out var cc))
|
|
||||||
usage.CacheCreationTokens += cc.GetInt64();
|
|
||||||
if (u.TryGetProperty("cache_read_input_tokens", out var cr))
|
|
||||||
usage.CacheReadTokens += cr.GetInt64();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,27 +1092,9 @@ public class AnthropicService
|
|||||||
}
|
}
|
||||||
else if (cbType == "web_search_tool_result")
|
else if (cbType == "web_search_tool_result")
|
||||||
{
|
{
|
||||||
// Gesamten Block als JSON speichern für die Conversation-History
|
|
||||||
var rawBlock = JsonSerializer.Deserialize<JsonElement>(cb.GetRawText());
|
var rawBlock = JsonSerializer.Deserialize<JsonElement>(cb.GetRawText());
|
||||||
webSearchResultBlocks[idx] = rawBlock;
|
webSearchResultBlocks[idx] = rawBlock;
|
||||||
|
webSearchSources[idx] = ExtractWebSearchSources(cb);
|
||||||
// Sources extrahieren für Frontend
|
|
||||||
var sources = new List<object>();
|
|
||||||
if (cb.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
foreach (var item in searchContent.EnumerateArray())
|
|
||||||
{
|
|
||||||
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
|
|
||||||
if (itemType == "web_search_result")
|
|
||||||
{
|
|
||||||
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
|
|
||||||
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
|
|
||||||
if (!string.IsNullOrEmpty(url))
|
|
||||||
sources.Add(new { url, title });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
webSearchSources[idx] = sources;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1452,6 +1291,91 @@ public class AnthropicService
|
|||||||
return (stopReason, assistantContent, executedTools, streamErrorInfo);
|
return (stopReason, assistantContent, executedTools, streamErrorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Shared Helpers ──
|
||||||
|
|
||||||
|
private const int MaxToolResultChars = 20_000;
|
||||||
|
|
||||||
|
private static string TruncateToolResult(string result)
|
||||||
|
{
|
||||||
|
return result.Length > MaxToolResultChars
|
||||||
|
? result[..MaxToolResultChars] + $"\n\n[... gekürzt, {result.Length - MaxToolResultChars} weitere Zeichen ...]"
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
|
||||||
|
/// </summary>
|
||||||
|
private static List<object> ExtractWebSearchSources(JsonElement block)
|
||||||
|
{
|
||||||
|
var sources = new List<object>();
|
||||||
|
if (block.TryGetProperty("content", out var searchContent) && searchContent.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var item in searchContent.EnumerateArray())
|
||||||
|
{
|
||||||
|
var itemType = item.TryGetProperty("type", out var it) ? it.GetString() : "";
|
||||||
|
if (itemType == "web_search_result")
|
||||||
|
{
|
||||||
|
var url = item.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
|
||||||
|
var title = item.TryGetProperty("title", out var tt) ? tt.GetString() ?? "" : "";
|
||||||
|
if (!string.IsNullOrEmpty(url))
|
||||||
|
sources.Add(new { url, title });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parst einen server_tool_use-Block und gibt (id, name, input) zurück.
|
||||||
|
/// </summary>
|
||||||
|
private static (string id, string name, JsonElement input) ParseServerToolUse(JsonElement block)
|
||||||
|
{
|
||||||
|
var toolId = block.GetProperty("id").GetString() ?? "";
|
||||||
|
var toolName = block.GetProperty("name").GetString() ?? "";
|
||||||
|
JsonElement inputEl;
|
||||||
|
try { inputEl = block.GetProperty("input"); }
|
||||||
|
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
|
||||||
|
return (toolId, toolName, inputEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aktualisiert den TokenUsage-Tracker aus einem usage-JSON-Element.
|
||||||
|
/// </summary>
|
||||||
|
private static void TrackUsage(TokenUsage usage, JsonElement u)
|
||||||
|
{
|
||||||
|
if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64();
|
||||||
|
if (u.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64();
|
||||||
|
if (u.TryGetProperty("cache_creation_input_tokens", out var cc)) usage.CacheCreationTokens += cc.GetInt64();
|
||||||
|
if (u.TryGetProperty("cache_read_input_tokens", out var cr)) usage.CacheReadTokens += cr.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fügt Tool-Definitionen und WebSearch zum Payload hinzu.
|
||||||
|
/// </summary>
|
||||||
|
private void AddToolsToPayload(Dictionary<string, object> payload, string workspace, string agentId, bool useMemory, string webSearchVersion, string toolChoice, int iteration, bool allowUserChoice = false, bool planMode = false)
|
||||||
|
{
|
||||||
|
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||||||
|
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
||||||
|
var hasTools = hasWorkspace || includeMemory;
|
||||||
|
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
|
||||||
|
|
||||||
|
if (!hasTools && !hasWebSearch) return;
|
||||||
|
|
||||||
|
var tools = new List<object>();
|
||||||
|
if (hasTools)
|
||||||
|
{
|
||||||
|
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice);
|
||||||
|
if (planMode)
|
||||||
|
allTools = _toolService.FilterReadOnlyTools(allTools);
|
||||||
|
tools.AddRange(allTools);
|
||||||
|
}
|
||||||
|
if (hasWebSearch)
|
||||||
|
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
|
||||||
|
payload["tools"] = tools;
|
||||||
|
var effectiveChoice = (!planMode && toolChoice == "any" && iteration == 1) ? "any" : "auto";
|
||||||
|
payload["tool_choice"] = new { type = effectiveChoice };
|
||||||
|
}
|
||||||
|
|
||||||
private HttpClient CreateClient()
|
private HttpClient CreateClient()
|
||||||
{
|
{
|
||||||
var client = _httpFactory.CreateClient();
|
var client = _httpFactory.CreateClient();
|
||||||
|
|||||||
+55
-108
@@ -114,29 +114,18 @@ public class ChatService
|
|||||||
if (!string.IsNullOrWhiteSpace(systemPromptText))
|
if (!string.IsNullOrWhiteSpace(systemPromptText))
|
||||||
systemPrompt.Add(new { type = "text", text = systemPromptText });
|
systemPrompt.Add(new { type = "text", text = systemPromptText });
|
||||||
|
|
||||||
// Preparer-Agent vorbereiten (falls konfiguriert)
|
// Sub-Agents vorbereiten (falls konfiguriert)
|
||||||
PreparerContext preparerContext = null;
|
var preparerContext = await BuildSubAgentContext(
|
||||||
if (!string.IsNullOrEmpty(session.PreparerAgentId))
|
session.PreparerAgentId, sessionId, workspace, session, user);
|
||||||
{
|
var reviewContext = await BuildSubAgentContext(
|
||||||
var preparerAgent = await _db.GetAgent(session.PreparerAgentId);
|
session.ReviewAgentId, sessionId, workspace, session, user);
|
||||||
if (preparerAgent != null)
|
var finalizerContext = await BuildSubAgentContext(
|
||||||
{
|
session.FinalizerAgentId, sessionId, workspace, session, user);
|
||||||
var preparerModelId = ResolveModelId(preparerAgent.Model);
|
|
||||||
var preparerMarked = await _db.GetMarkedMessages(sessionId);
|
|
||||||
var preparerMemDir = _memoryService.GetAgentMemoryDir(preparerAgent.AgentId);
|
|
||||||
var preparerSkills = await _db.GetAgentSkills(preparerAgent.AgentId);
|
|
||||||
var preparerWorkspace = workspace;
|
|
||||||
var preparerSystemText = SystemPromptBuilder.Build(
|
|
||||||
preparerAgent, session, preparerMarked, _memoryService, preparerMemDir,
|
|
||||||
preparerSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
|
||||||
resolvedWorkspace: preparerWorkspace);
|
|
||||||
|
|
||||||
var preparerPrompt = new List<object>();
|
if (preparerContext != null)
|
||||||
if (!string.IsNullOrWhiteSpace(preparerSystemText))
|
{
|
||||||
preparerPrompt.Add(new { type = "text", text = preparerSystemText });
|
preparerContext.AllowUserChoice = preparerContext.Agent?.AllowUserChoice ?? false;
|
||||||
|
preparerContext.SystemPrompt.Add(new { type = "text", text = @"## KRITISCH: Deine Rolle als Vorbereiter-Agent
|
||||||
// Preparer-Instruktion: erklärt Vorbereiter-Mechanismus
|
|
||||||
preparerPrompt.Add(new { type = "text", text = @"## KRITISCH: Deine Rolle als Vorbereiter-Agent
|
|
||||||
|
|
||||||
Du bist ein Vorbereiter-Agent. Deine Aufgabe: Die Anfrage des Users aufbereiten, deine Fähigkeiten nutzen, und das Ergebnis an den Session-Agent weiterleiten.
|
Du bist ein Vorbereiter-Agent. Deine Aufgabe: Die Anfrage des Users aufbereiten, deine Fähigkeiten nutzen, und das Ergebnis an den Session-Agent weiterleiten.
|
||||||
|
|
||||||
@@ -165,95 +154,11 @@ Rückfrage (NUR mit [QUESTION] – stoppt die Weiterleitung):
|
|||||||
[QUESTION]
|
[QUESTION]
|
||||||
Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||||
```" });
|
```" });
|
||||||
|
|
||||||
var preparerHasTools = !string.IsNullOrEmpty(preparerWorkspace) || preparerAgent.UseMemory;
|
|
||||||
preparerContext = new PreparerContext
|
|
||||||
{
|
|
||||||
ModelId = preparerModelId,
|
|
||||||
SystemPrompt = preparerPrompt,
|
|
||||||
AgentId = preparerAgent.AgentId,
|
|
||||||
Workspace = preparerWorkspace,
|
|
||||||
UseMemory = preparerAgent.UseMemory,
|
|
||||||
WebSearchVersion = preparerAgent.WebSearchVersion,
|
|
||||||
ToolChoice = preparerHasTools ? "any" : "auto",
|
|
||||||
AllowUserChoice = preparerAgent.AllowUserChoice,
|
|
||||||
Agent = preparerAgent
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Review-Agent vorbereiten (falls konfiguriert)
|
if (reviewContext != null)
|
||||||
ReviewContext reviewContext = null;
|
reviewContext.MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3;
|
||||||
if (!string.IsNullOrEmpty(session.ReviewAgentId))
|
|
||||||
{
|
|
||||||
var reviewAgent = await _db.GetAgent(session.ReviewAgentId);
|
|
||||||
if (reviewAgent != null)
|
|
||||||
{
|
|
||||||
var reviewModelId = ResolveModelId(reviewAgent.Model);
|
|
||||||
var reviewMarked = await _db.GetMarkedMessages(sessionId);
|
|
||||||
var reviewMemDir = _memoryService.GetAgentMemoryDir(reviewAgent.AgentId);
|
|
||||||
var reviewSkills = await _db.GetAgentSkills(reviewAgent.AgentId);
|
|
||||||
var reviewWorkspace = workspace; // gleicher Workspace wie Worker
|
|
||||||
var reviewSystemText = SystemPromptBuilder.Build(
|
|
||||||
reviewAgent, session, reviewMarked, _memoryService, reviewMemDir,
|
|
||||||
reviewSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
|
||||||
resolvedWorkspace: reviewWorkspace);
|
|
||||||
|
|
||||||
var reviewPrompt = new List<object>();
|
|
||||||
if (!string.IsNullOrWhiteSpace(reviewSystemText))
|
|
||||||
reviewPrompt.Add(new { type = "text", text = reviewSystemText });
|
|
||||||
|
|
||||||
var reviewHasTools = !string.IsNullOrEmpty(reviewWorkspace) || reviewAgent.UseMemory;
|
|
||||||
reviewContext = new ReviewContext
|
|
||||||
{
|
|
||||||
ModelId = reviewModelId,
|
|
||||||
SystemPrompt = reviewPrompt,
|
|
||||||
AgentId = reviewAgent.AgentId,
|
|
||||||
Workspace = reviewWorkspace,
|
|
||||||
UseMemory = reviewAgent.UseMemory,
|
|
||||||
WebSearchVersion = reviewAgent.WebSearchVersion,
|
|
||||||
ToolChoice = reviewHasTools ? "any" : "auto",
|
|
||||||
MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3,
|
|
||||||
Agent = reviewAgent
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finalizer-Agent vorbereiten (falls konfiguriert)
|
|
||||||
FinalizerContext finalizerContext = null;
|
|
||||||
if (!string.IsNullOrEmpty(session.FinalizerAgentId))
|
|
||||||
{
|
|
||||||
var finalizerAgent = await _db.GetAgent(session.FinalizerAgentId);
|
|
||||||
if (finalizerAgent != null)
|
|
||||||
{
|
|
||||||
var finalizerModelId = ResolveModelId(finalizerAgent.Model);
|
|
||||||
var finalizerMarked = await _db.GetMarkedMessages(sessionId);
|
|
||||||
var finalizerMemDir = _memoryService.GetAgentMemoryDir(finalizerAgent.AgentId);
|
|
||||||
var finalizerSkills = await _db.GetAgentSkills(finalizerAgent.AgentId);
|
|
||||||
var finalizerWorkspace = workspace;
|
|
||||||
var finalizerSystemText = SystemPromptBuilder.Build(
|
|
||||||
finalizerAgent, session, finalizerMarked, _memoryService, finalizerMemDir,
|
|
||||||
finalizerSkills, _skillService, userPersona: user?.GetEffectivePersona(),
|
|
||||||
resolvedWorkspace: finalizerWorkspace);
|
|
||||||
|
|
||||||
var finalizerPrompt = new List<object>();
|
|
||||||
if (!string.IsNullOrWhiteSpace(finalizerSystemText))
|
|
||||||
finalizerPrompt.Add(new { type = "text", text = finalizerSystemText });
|
|
||||||
|
|
||||||
var finalizerHasTools = !string.IsNullOrEmpty(finalizerWorkspace) || finalizerAgent.UseMemory;
|
|
||||||
finalizerContext = new FinalizerContext
|
|
||||||
{
|
|
||||||
ModelId = finalizerModelId,
|
|
||||||
SystemPrompt = finalizerPrompt,
|
|
||||||
AgentId = finalizerAgent.AgentId,
|
|
||||||
Workspace = finalizerWorkspace,
|
|
||||||
UseMemory = finalizerAgent.UseMemory,
|
|
||||||
WebSearchVersion = finalizerAgent.WebSearchVersion,
|
|
||||||
ToolChoice = finalizerHasTools ? "any" : "auto",
|
|
||||||
Agent = finalizerAgent
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ChatContext
|
return new ChatContext
|
||||||
{
|
{
|
||||||
@@ -531,4 +436,46 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
|||||||
|
|
||||||
return history;
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baut einen SubAgentContext für Preparer/Review/Finalizer.
|
||||||
|
/// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<SubAgentContext> BuildSubAgentContext(
|
||||||
|
string agentId, string sessionId, string workspace, SessionRecord session, UserRecord user)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(agentId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var agent = await _db.GetAgent(agentId);
|
||||||
|
if (agent == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var modelId = ResolveModelId(agent.Model);
|
||||||
|
var markedMessages = await _db.GetMarkedMessages(sessionId);
|
||||||
|
var memDir = _memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||||
|
var skills = await _db.GetAgentSkills(agent.AgentId);
|
||||||
|
var systemText = SystemPromptBuilder.Build(
|
||||||
|
agent, session, markedMessages, _memoryService, memDir,
|
||||||
|
skills, _skillService, userPersona: user?.GetEffectivePersona(),
|
||||||
|
resolvedWorkspace: workspace);
|
||||||
|
|
||||||
|
var prompt = new List<object>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(systemText))
|
||||||
|
prompt.Add(new { type = "text", text = systemText });
|
||||||
|
|
||||||
|
var hasTools = !string.IsNullOrEmpty(workspace) || agent.UseMemory;
|
||||||
|
return new SubAgentContext
|
||||||
|
{
|
||||||
|
ModelId = modelId,
|
||||||
|
SystemPrompt = prompt,
|
||||||
|
AgentId = agent.AgentId,
|
||||||
|
Workspace = workspace,
|
||||||
|
UseMemory = agent.UseMemory,
|
||||||
|
WebSearchVersion = agent.WebSearchVersion,
|
||||||
|
ToolChoice = hasTools ? "any" : "auto",
|
||||||
|
Agent = agent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -338,7 +338,7 @@ public class DatabaseService
|
|||||||
|
|
||||||
foreach (var (agentId, avatar) in agentsWithoutThumb)
|
foreach (var (agentId, avatar) in agentsWithoutThumb)
|
||||||
{
|
{
|
||||||
var small = ResizeDataUrlToWebP(avatar, smallSize, smallQuality);
|
var small = ConvertDataUrlToWebP(avatar, smallQuality, maxSide: smallSize);
|
||||||
if (small != null)
|
if (small != null)
|
||||||
conn.Execute("UPDATE agents SET avatar_small = @small WHERE agent_id = @agentId", new { small, agentId });
|
conn.Execute("UPDATE agents SET avatar_small = @small WHERE agent_id = @agentId", new { small, agentId });
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ public class DatabaseService
|
|||||||
|
|
||||||
foreach (var (sessionId, image) in sessionsWithoutThumb)
|
foreach (var (sessionId, image) in sessionsWithoutThumb)
|
||||||
{
|
{
|
||||||
var small = ResizeDataUrlToWebP(image, smallSize, smallQuality);
|
var small = ConvertDataUrlToWebP(image, smallQuality, maxSide: smallSize);
|
||||||
if (small != null)
|
if (small != null)
|
||||||
conn.Execute("UPDATE sessions SET image_small = @small WHERE session_id = @sessionId", new { small, sessionId });
|
conn.Execute("UPDATE sessions SET image_small = @small WHERE session_id = @sessionId", new { small, sessionId });
|
||||||
}
|
}
|
||||||
@@ -359,7 +359,10 @@ public class DatabaseService
|
|||||||
Console.WriteLine($"[Migration] Generated {agentsWithoutThumb.Count} agent thumbnails and {sessionsWithoutThumb.Count} session thumbnails");
|
Console.WriteLine($"[Migration] Generated {agentsWithoutThumb.Count} agent thumbnails and {sessionsWithoutThumb.Count} session thumbnails");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResizeDataUrlToWebP(string dataUrl, int maxSide, int quality)
|
/// <summary>
|
||||||
|
/// Konvertiert ein DataUrl-Bild zu WebP. Optional Resize wenn maxSide > 0.
|
||||||
|
/// </summary>
|
||||||
|
private static string ConvertDataUrlToWebP(string dataUrl, int quality, int maxSide = 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -370,7 +373,7 @@ public class DatabaseService
|
|||||||
var bytes = Convert.FromBase64String(base64);
|
var bytes = Convert.FromBase64String(base64);
|
||||||
|
|
||||||
using var image = Image.Load(bytes);
|
using var image = Image.Load(bytes);
|
||||||
if (image.Width > maxSide || image.Height > maxSide)
|
if (maxSide > 0 && (image.Width > maxSide || image.Height > maxSide))
|
||||||
{
|
{
|
||||||
image.Mutate(x => x.Resize(new ResizeOptions
|
image.Mutate(x => x.Resize(new ResizeOptions
|
||||||
{
|
{
|
||||||
@@ -390,28 +393,6 @@ public class DatabaseService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ConvertDataUrlToWebP(string dataUrl, int quality)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var commaIndex = dataUrl.IndexOf(',');
|
|
||||||
if (commaIndex < 0) return null;
|
|
||||||
|
|
||||||
var base64 = dataUrl.Substring(commaIndex + 1);
|
|
||||||
var bytes = Convert.FromBase64String(base64);
|
|
||||||
|
|
||||||
using var image = Image.Load(bytes);
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
image.Save(ms, new WebpEncoder { Quality = quality });
|
|
||||||
|
|
||||||
return "data:image/webp;base64," + Convert.ToBase64String(ms.ToArray());
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null; // Beschädigte Bilder überspringen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
||||||
{
|
{
|
||||||
try { conn.Execute($"ALTER TABLE {table} ADD COLUMN {column} {type}"); }
|
try { conn.Execute($"ALTER TABLE {table} ADD COLUMN {column} {type}"); }
|
||||||
@@ -573,13 +554,6 @@ public class DatabaseService
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SessionRecord>> GetSessions(string userId)
|
|
||||||
{
|
|
||||||
using var conn = CreateConnection();
|
|
||||||
return await conn.QueryAsync<SessionRecord>(
|
|
||||||
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<SessionWithStats>> GetSessionsWithStats(string userId)
|
public async Task<List<SessionWithStats>> GetSessionsWithStats(string userId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
@@ -1303,23 +1277,6 @@ public class DatabaseService
|
|||||||
new { sessionId, chatId, now });
|
new { sessionId, chatId, now });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetSessionTelegramChatId(string sessionId, string chatId)
|
|
||||||
{
|
|
||||||
using var conn = CreateConnection();
|
|
||||||
var now = DateTime.UtcNow.ToString("o");
|
|
||||||
await conn.ExecuteAsync(
|
|
||||||
"UPDATE sessions SET telegram_chat_id = @chatId, updated_at = @now WHERE session_id = @sessionId",
|
|
||||||
new { sessionId, chatId, now });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearSessionTelegramChatId(string sessionId)
|
|
||||||
{
|
|
||||||
using var conn = CreateConnection();
|
|
||||||
await conn.ExecuteAsync(
|
|
||||||
"UPDATE sessions SET telegram_chat_id = NULL, updated_at = @now WHERE session_id = @sessionId",
|
|
||||||
new { sessionId, now = DateTime.UtcNow.ToString("o") });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<SessionRecord>> GetSessionsByTelegramBot(string botId)
|
public async Task<IEnumerable<SessionRecord>> GetSessionsByTelegramBot(string botId)
|
||||||
{
|
{
|
||||||
using var conn = CreateConnection();
|
using var conn = CreateConnection();
|
||||||
@@ -1345,14 +1302,6 @@ public class DatabaseService
|
|||||||
new { botId });
|
new { botId });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SessionRecord>> GetUserSessions(string userId)
|
|
||||||
{
|
|
||||||
using var conn = CreateConnection();
|
|
||||||
return await conn.QueryAsync<SessionRecord>(
|
|
||||||
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC",
|
|
||||||
new { userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Cronjobs ──
|
// ── Cronjobs ──
|
||||||
|
|
||||||
public async Task<CronjobRecord> CreateCronjob(CronjobRecord cronjob)
|
public async Task<CronjobRecord> CreateCronjob(CronjobRecord cronjob)
|
||||||
|
|||||||
@@ -62,4 +62,37 @@ public class ImageService
|
|||||||
|
|
||||||
return "data:image/webp;base64," + webpBase64;
|
return "data:image/webp;base64," + webpBase64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generiert ein WebP-Thumbnail (max 320px) mit Caching.
|
||||||
|
/// Gibt den Pfad zum Thumbnail zurück, oder null bei Fehler.
|
||||||
|
/// </summary>
|
||||||
|
public static async System.Threading.Tasks.Task<string> GenerateThumbnail(string originalPath, string cacheDir, int maxSize = 320, int quality = 75)
|
||||||
|
{
|
||||||
|
var thumbFilename = Path.GetFileNameWithoutExtension(originalPath) + ".webp";
|
||||||
|
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
||||||
|
|
||||||
|
if (File.Exists(thumbPath) && File.GetLastWriteTimeUtc(originalPath) <= File.GetLastWriteTimeUtc(thumbPath))
|
||||||
|
return thumbPath;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
using var image = await Image.LoadAsync(originalPath);
|
||||||
|
if (image.Width > maxSize || image.Height > maxSize)
|
||||||
|
{
|
||||||
|
image.Mutate(x => x.Resize(new ResizeOptions
|
||||||
|
{
|
||||||
|
Size = new Size(maxSize, maxSize),
|
||||||
|
Mode = ResizeMode.Max
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await image.SaveAsync(thumbPath, new WebpEncoder { Quality = quality });
|
||||||
|
return thumbPath;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
- [x] Größere Tasks brechen irgendwann ab, könnte an der Limitierung von Antrophic/Claude liegen. Jedoch sollte wird es scheinbar nicht behandelt: Der Status bleibt auf "Streaming..." aber es passiert nichts mehr
|
- [x] Größere Tasks brechen irgendwann ab, könnte an der Limitierung von Antrophic/Claude liegen. Jedoch sollte wird es scheinbar nicht behandelt: Der Status bleibt auf "Streaming..." aber es passiert nichts mehr
|
||||||
- [x] Session -> Chat öffnen im Frontend soll nicht möglich sein, wenn kein Agent gesetzt ist
|
- [x] Session -> Chat öffnen im Frontend soll nicht möglich sein, wenn kein Agent gesetzt ist
|
||||||
- [x] Falsche Nachrichtenanzahl im Dashboard (tool uses werden fälschicherweise mitgezählt) - dasselbe bei "Ältere Nachrichten laden" im Chat
|
- [x] Falsche Nachrichtenanzahl im Dashboard (tool uses werden fälschicherweise mitgezählt) - dasselbe bei "Ältere Nachrichten laden" im Chat
|
||||||
|
- [ ] Beim Benutzen vom Tool "todo_write": beim streamen ist der text abgeschnitten (limitiert), nach dem poll (history laden) passt es wieder. es soll beim stream auch vollständig angezeigt werden und nicht mit "..." enden
|
||||||
|
- [ ] Manchmal passiert es wenn ein Agent die Memory-Funktion hat, dass er sich selbst Memories einträgt - das ist auch ok so, aber, manchmal werden "dumme" erinnerungen gesetzt, die nur session-bezogen sind (und nicht agent-bezogen) und diese sind dann fast immer einfach nur copy&paste aus dem session system-prompt. Woran kann das liegen bzw. wie kann man das optimieren?
|
||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
- [x] System-Prompt anpassen. Explizit auf Workspace-Pfad hinweisen, weil Model halluziniert oft und will in den nicht vorhandenen Pfad /workspace wechseln
|
- [x] System-Prompt anpassen. Explizit auf Workspace-Pfad hinweisen, weil Model halluziniert oft und will in den nicht vorhandenen Pfad /workspace wechseln
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
|
|
||||||
## Ideas / Backlog
|
## Ideas / Backlog
|
||||||
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
|
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
|
||||||
- [ ] Subagents: Möglichkeit Subagents zu starten und zu orchestrieren
|
- [ ] Claude (Sub-)Agents: Claude hat so ein Feature Build-in, kann man dieses Feature nutzen? bzw. macht es Sinn dieses Feature zu benutzen und kann man es einbauen?
|
||||||
|
|
||||||
## Features aus Devy übernehmen
|
## Features aus Devy übernehmen
|
||||||
- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "denkt nach..."
|
- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "denkt nach..."
|
||||||
|
|||||||
Reference in new issue
Block a user