Opus 4.5+ and Sonnet 4.6+ support a new "effort" knob that controls how
much budget the model spends on thinking and tool calls. Plumb it through
end to end so each agent can pick its own level.
Storage & shape:
- New TEXT column agents.effort (default '') added via TryAddColumn.
- AgentRecord.Effort + INSERT/UPDATE in DatabaseService.Agents.cs.
- ChatContext.Effort + SubAgentContext.Effort so the value travels with the
request alongside model and tool settings.
Agent editor:
- Effort dropdown above git-commit behavior in the agent form (low/medium/
high/xhigh/max plus empty = "use default"). Always visible; on OpenAI
providers the value is sent but silently ignored.
- agent-page.js load/save and AgentEndpoint create/update wired up.
Anthropic API path:
- StreamChat and RunNonStreamingToolLoop add output_config: { effort } when
effort is set and not "high" – empty/null/"high" means "omit and let the
API pick its default".
- Sub-agents (preparer/review/finalizer) get the parameter through
RunAgentChat so each step uses its own agent's effort.
Anthropic CLI path:
- ClaudeCliService.ChatStreamingAsync takes an effort parameter and exports
CLAUDE_CODE_EFFORT_LEVEL on the spawned process. Env-var beats the
--effort flag because it also covers "max" and "ultracode".
- StreamChatViaCli, ChatNonStreamingViaCli, RunSubAgentViaCli, and the
ChatEndpoint anthropic branches all hand the value through.
ARCHITECTURE.md / ARCHITECTURE_QUICK.md updated with the new field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
211 lines
10 KiB
C#
211 lines
10 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using Gott.Extensions;
|
|
using Gott.Models;
|
|
using Gott.Services;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
|
|
namespace Gott.Endpoints;
|
|
|
|
public static class AgentEndpoint
|
|
{
|
|
public static void MapAgentEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
app.MapGet("/api/agents", HandleList);
|
|
app.MapGet("/api/agents/available", HandleListAvailable);
|
|
app.MapGet("/api/agents/{agentId}", HandleGet);
|
|
app.MapPost("/api/agents", HandleCreate);
|
|
app.MapPut("/api/agents/{agentId}", HandleUpdate);
|
|
app.MapDelete("/api/agents/{agentId}", HandleDelete);
|
|
}
|
|
|
|
private static async Task HandleList(HttpContext ctx, DatabaseService db, SkillService skillService)
|
|
{
|
|
var userId = ctx.GetUserId();
|
|
var agents = await db.GetAgents(userId);
|
|
await ctx.Response.WriteAsJsonAsync(await MapAgentList(agents, userId, db, skillService));
|
|
}
|
|
|
|
private static async Task HandleListAvailable(HttpContext ctx, DatabaseService db, SkillService skillService)
|
|
{
|
|
var userId = ctx.GetUserId();
|
|
var agents = await db.GetAvailableAgents(userId);
|
|
await ctx.Response.WriteAsJsonAsync(await MapAgentList(agents, userId, db, skillService, includeOwner: true));
|
|
}
|
|
|
|
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>();
|
|
|
|
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.UseAutoContext,
|
|
a.UseGitHubSearch,
|
|
a.UseSerperSearch,
|
|
a.UseWebSearch,
|
|
a.WebSearchVersion,
|
|
a.AllowUserChoice,
|
|
a.TaskInstruction,
|
|
a.Effort,
|
|
a.AvatarScale,
|
|
a.AvatarOffsetX,
|
|
a.AvatarOffsetY,
|
|
a.IsPublic,
|
|
a.IsDeleted,
|
|
a.CreatedAt,
|
|
a.UpdatedAt,
|
|
IsOwner = includeOwner ? (bool?)(a.UserId == userId) : null,
|
|
Skills = skills
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
|
|
{
|
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
|
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
|
|
|
var agent = await db.GetAgent(agentId);
|
|
if (agent == null || agent.UserId != ctx.GetUserId()) { ctx.Response.StatusCode = 404; return; }
|
|
|
|
await ctx.Response.WriteAsJsonAsync(agent);
|
|
}
|
|
|
|
private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
|
{
|
|
var userId = ctx.GetUserId();
|
|
var user = await db.GetUser(userId);
|
|
if (user != null && !user.CanCreateAgents && !user.IsAdmin)
|
|
{
|
|
ctx.Response.StatusCode = 403;
|
|
await ctx.Response.WriteAsJsonAsync(new { error = "Keine Berechtigung zum Erstellen von Agents" });
|
|
return;
|
|
}
|
|
|
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
|
|
|
var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null;
|
|
|
|
string customCommands = null;
|
|
if (body.TryGetProperty("customCommands", out var ccEl))
|
|
customCommands = ccEl.ValueKind == JsonValueKind.Null ? null : ccEl.GetRawText();
|
|
|
|
var agent = new AgentRecord
|
|
{
|
|
UserId = userId,
|
|
Name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "Neuer Agent" : "Neuer Agent",
|
|
Avatar = imageService.ResizeDataUrl(avatarRaw),
|
|
AvatarSmall = imageService.ResizeDataUrlSmall(avatarRaw),
|
|
Description = body.TryGetProperty("description", out var d) ? d.GetString() : null,
|
|
Model = body.TryGetProperty("model", out var m) ? m.GetString() ?? "sonnet" : "sonnet",
|
|
CustomCommands = customCommands,
|
|
AutoCommit = body.TryGetProperty("autoCommit", out var ac) ? ac.GetString() ?? "" : "",
|
|
UseSoul = !body.TryGetProperty("useSoul", out var us) || us.GetBoolean(),
|
|
UseIdentity = !body.TryGetProperty("useIdentity", out var ui) || ui.GetBoolean(),
|
|
UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(),
|
|
UseMemory = !body.TryGetProperty("useMemory", out var um) || um.GetBoolean(),
|
|
UseAutoContext = !body.TryGetProperty("useAutoContext", out var uac) || uac.GetBoolean(),
|
|
UseGitHubSearch = body.TryGetProperty("useGitHubSearch", out var ughs) && ughs.GetBoolean(),
|
|
UseSerperSearch = body.TryGetProperty("useSerperSearch", out var usps) && usps.GetBoolean(),
|
|
UseWebSearch = body.TryGetProperty("webSearchVersion", out var wsv) && !string.IsNullOrEmpty(wsv.GetString()),
|
|
WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "",
|
|
AllowUserChoice = !body.TryGetProperty("allowUserChoice", out var auc) || auc.GetBoolean(),
|
|
TaskInstruction = body.TryGetProperty("taskInstruction", out var ti) ? ti.GetString() : null,
|
|
Effort = body.TryGetProperty("effort", out var ef) ? ef.GetString() ?? "" : "",
|
|
AvatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0,
|
|
AvatarOffsetX = body.TryGetProperty("avatarOffsetX", out var aox) ? aox.GetDouble() : 0,
|
|
AvatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0,
|
|
IsPublic = body.TryGetProperty("isPublic", out var ip) && ip.GetBoolean()
|
|
};
|
|
|
|
agent = await db.CreateAgent(agent);
|
|
memoryService.InitializeAgentFiles(agent.AgentId, agent.Name);
|
|
await ctx.Response.WriteAsJsonAsync(agent);
|
|
}
|
|
|
|
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService, MemoryService memoryService)
|
|
{
|
|
var userId = ctx.GetUserId();
|
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
|
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
|
|
|
var agent = await db.GetAgent(agentId);
|
|
if (agent == null || agent.UserId != userId) { ctx.Response.StatusCode = 404; return; }
|
|
|
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
|
|
|
if (body.TryGetProperty("name", out var n)) agent.Name = n.GetString();
|
|
if (body.TryGetProperty("avatar", out var av))
|
|
{
|
|
agent.Avatar = imageService.ResizeDataUrl(av.GetString());
|
|
agent.AvatarSmall = imageService.ResizeDataUrlSmall(av.GetString());
|
|
}
|
|
if (body.TryGetProperty("description", out var d)) agent.Description = d.GetString();
|
|
if (body.TryGetProperty("model", out var m)) agent.Model = m.GetString();
|
|
if (body.TryGetProperty("customCommands", out var cc))
|
|
agent.CustomCommands = cc.ValueKind == JsonValueKind.Null ? null : cc.GetRawText();
|
|
if (body.TryGetProperty("autoCommit", out var ac)) agent.AutoCommit = ac.GetString() ?? "";
|
|
if (body.TryGetProperty("useSoul", out var us)) agent.UseSoul = us.GetBoolean();
|
|
if (body.TryGetProperty("useIdentity", out var ui)) agent.UseIdentity = ui.GetBoolean();
|
|
if (body.TryGetProperty("useUserInfo", out var uu)) agent.UseUserInfo = uu.GetBoolean();
|
|
if (body.TryGetProperty("useMemory", out var um)) agent.UseMemory = um.GetBoolean();
|
|
if (body.TryGetProperty("useAutoContext", out var uac)) agent.UseAutoContext = uac.GetBoolean();
|
|
if (body.TryGetProperty("useGitHubSearch", out var ughs)) agent.UseGitHubSearch = ughs.GetBoolean();
|
|
if (body.TryGetProperty("useSerperSearch", out var usps)) agent.UseSerperSearch = usps.GetBoolean();
|
|
if (body.TryGetProperty("webSearchVersion", out var wsv))
|
|
{
|
|
agent.WebSearchVersion = wsv.GetString() ?? "";
|
|
agent.UseWebSearch = !string.IsNullOrEmpty(agent.WebSearchVersion);
|
|
}
|
|
if (body.TryGetProperty("allowUserChoice", out var auc)) agent.AllowUserChoice = auc.GetBoolean();
|
|
if (body.TryGetProperty("taskInstruction", out var ti)) agent.TaskInstruction = ti.GetString();
|
|
if (body.TryGetProperty("effort", out var ef)) agent.Effort = ef.GetString() ?? "";
|
|
if (body.TryGetProperty("avatarScale", out var asc)) agent.AvatarScale = asc.GetDouble();
|
|
if (body.TryGetProperty("avatarOffsetX", out var aox)) agent.AvatarOffsetX = aox.GetDouble();
|
|
if (body.TryGetProperty("avatarOffsetY", out var aoy)) agent.AvatarOffsetY = aoy.GetDouble();
|
|
if (body.TryGetProperty("isPublic", out var ip)) agent.IsPublic = ip.GetBoolean();
|
|
|
|
await db.UpdateAgent(agent);
|
|
memoryService.InitializeAgentFiles(agentId, agent.Name);
|
|
agent = await db.GetAgent(agentId);
|
|
await ctx.Response.WriteAsJsonAsync(agent);
|
|
}
|
|
|
|
private static async Task HandleDelete(HttpContext ctx, DatabaseService db)
|
|
{
|
|
var userId = ctx.GetUserId();
|
|
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
|
|
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
|
|
|
|
await db.DeleteAgent(agentId, userId);
|
|
ctx.Response.StatusCode = 204;
|
|
}
|
|
}
|