forked from admin/VolcanicAgenticCore
84 lines
4.4 KiB
C#
84 lines
4.4 KiB
C#
namespace Volcanic.AgenticCore.Telegram.Triggers;
|
|
|
|
/// <summary>
|
|
/// The <c>Telegram:Triggers</c> section: group watching with topic triggers classified by TypeSafe's Jev through
|
|
/// OpenRouter. On exactly when <see cref="ApiKey"/> is set.
|
|
/// </summary>
|
|
public sealed class TriggerOptions
|
|
{
|
|
public const string DefaultModel = "typesafe/jev-1.13";
|
|
public const string DefaultBaseUrl = "https://openrouter.ai/api/v1";
|
|
|
|
/// <summary>A snooze batch is split into Jev calls of at most this many characters of state JSON (Jev reads 32k tokens).</summary>
|
|
public const int ChunkChars = 40_000;
|
|
|
|
/// <summary>A snooze digest longer than this goes to a file in the owner's workspace instead of the prompt.</summary>
|
|
public const int DigestChars = 30_000;
|
|
|
|
/// <summary>A 429's <c>Retry-After</c> is honoured up to this.</summary>
|
|
public static readonly TimeSpan MaxRetryAfter = TimeSpan.FromSeconds(30);
|
|
|
|
/// <summary>Falls back to <c>OPENROUTER_API_KEY</c>. Empty turns triggers (and group watching) off.</summary>
|
|
public string ApiKey { get; set; } = "";
|
|
|
|
/// <summary>Falls back to <c>TRIGGER_MODEL</c>, then <see cref="DefaultModel"/>.</summary>
|
|
public string Model { get; set; } = "";
|
|
|
|
/// <summary>OpenRouter's API root; requests go to <c>{BaseUrl}/systemone</c>.</summary>
|
|
public string BaseUrl { get; set; } = DefaultBaseUrl;
|
|
|
|
/// <summary>The threshold of a trigger created without one.</summary>
|
|
public double DefaultThreshold { get; set; } = 0.7;
|
|
|
|
/// <summary>Matches of one trigger within this window (opened by the first) reach the owner as one prompt.</summary>
|
|
public TimeSpan Debounce { get; set; } = TimeSpan.FromSeconds(10);
|
|
|
|
/// <summary>Retries after the first Jev attempt (network errors, timeouts, 408, 429, 5xx).</summary>
|
|
public int MaxRetries { get; set; } = 3;
|
|
|
|
/// <summary>Jev calls in flight at once.</summary>
|
|
public int MaxConcurrency { get; set; } = 4;
|
|
|
|
public int MaxTriggersPerChat { get; set; } = 20;
|
|
|
|
public TimeSpan MaxSnooze { get; set; } = TimeSpan.FromDays(30);
|
|
|
|
/// <summary>Messages kept per snoozed trigger; past it the oldest are dropped.</summary>
|
|
public int MaxQueuedPerTrigger { get; set; } = 5000;
|
|
|
|
/// <summary>The wait before the first retry, doubled for each next one. Internal so tests can zero it.</summary>
|
|
internal TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(1);
|
|
|
|
public bool IsEnabled => !string.IsNullOrWhiteSpace(ApiKey);
|
|
|
|
public string ResolvedModel => string.IsNullOrWhiteSpace(Model) ? DefaultModel : Model.Trim();
|
|
|
|
/// <summary>The startup log line (never the key).</summary>
|
|
public string Describe() => IsEnabled
|
|
? $"Group triggers: on, {ResolvedModel} via {BaseUrl.TrimEnd('/')}/systemone"
|
|
: "Group triggers: off (no OPENROUTER_API_KEY); group messages are ignored";
|
|
|
|
/// <summary>The environment fills only empty settings; an empty variable counts as unset.</summary>
|
|
public void ApplyEnvironment(Func<string, string?> getEnv)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(getEnv);
|
|
if (string.IsNullOrWhiteSpace(ApiKey) && getEnv("OPENROUTER_API_KEY") is { } key && !string.IsNullOrWhiteSpace(key)) ApiKey = key.Trim();
|
|
if (string.IsNullOrWhiteSpace(Model) && getEnv("TRIGGER_MODEL") is { } model && !string.IsNullOrWhiteSpace(model)) Model = model.Trim();
|
|
}
|
|
|
|
public void Validate(List<string> errors)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(errors);
|
|
// On Linux "/v1" parses as an absolute file:// URI; the scheme check rejects it.
|
|
if (!Uri.TryCreate(BaseUrl, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
|
|
errors.Add("Telegram:Triggers:BaseUrl must be an absolute http(s) URL.");
|
|
if (DefaultThreshold is <= 0 or >= 1) errors.Add("Telegram:Triggers:DefaultThreshold must be between 0 and 1.");
|
|
if (Debounce < TimeSpan.Zero) errors.Add("Telegram:Triggers:Debounce must not be negative.");
|
|
if (MaxRetries < 0) errors.Add("Telegram:Triggers:MaxRetries must not be negative.");
|
|
if (MaxConcurrency <= 0) errors.Add("Telegram:Triggers:MaxConcurrency must be positive.");
|
|
if (MaxTriggersPerChat <= 0) errors.Add("Telegram:Triggers:MaxTriggersPerChat must be positive.");
|
|
if (MaxSnooze <= TimeSpan.Zero) errors.Add("Telegram:Triggers:MaxSnooze must be positive.");
|
|
if (MaxQueuedPerTrigger <= 0) errors.Add("Telegram:Triggers:MaxQueuedPerTrigger must be positive.");
|
|
}
|
|
}
|