add memory
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 16s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 16s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,6 +39,7 @@ project.lock.json
|
|||||||
|
|
||||||
# secrets
|
# secrets
|
||||||
.env
|
.env
|
||||||
|
data/
|
||||||
|
|
||||||
# OS generated files
|
# OS generated files
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class LlmCommands : InteractionModuleBase<SocketInteractionContext>
|
|||||||
{
|
{
|
||||||
var accumulated = "";
|
var accumulated = "";
|
||||||
|
|
||||||
await foreach (var text in _llmService.SendMessageAsync(channelId, userName, prompt))
|
await foreach (var text in _llmService.SendMessageAsync(channelId, Context.User.Id, userName, prompt))
|
||||||
{
|
{
|
||||||
accumulated = text;
|
accumulated = text;
|
||||||
}
|
}
|
||||||
|
|||||||
6
Models/MemoryModels.cs
Normal file
6
Models/MemoryModels.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TentacleBot.Models;
|
||||||
|
|
||||||
|
public record UserRule(int Id, string UserId, string RuleText, DateTime CreatedAt);
|
||||||
|
public record MemoryEntry(int Id, string UserId, string Content, string? Category, int Importance, DateTime CreatedAt);
|
||||||
|
public record ToolCall(string Id, string Type, ToolCallFunction Function);
|
||||||
|
public record ToolCallFunction(string Name, string Arguments);
|
||||||
16
Program.cs
16
Program.cs
@@ -6,6 +6,8 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TentacleBot.Services;
|
using TentacleBot.Services;
|
||||||
|
using TentacleBot.Services.Memory;
|
||||||
|
using TentacleBot.Services.Tools;
|
||||||
using TentacleBot.Commands;
|
using TentacleBot.Commands;
|
||||||
|
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
@@ -36,11 +38,25 @@ var services = new ServiceCollection()
|
|||||||
.AddHttpClient()
|
.AddHttpClient()
|
||||||
.AddSingleton<PalworldApiService>();
|
.AddSingleton<PalworldApiService>();
|
||||||
|
|
||||||
|
// Initialize memory database
|
||||||
|
var dbPath = configuration["Memory:DatabasePath"] ?? "data/tentaclebot.db";
|
||||||
|
var dbInit = new DatabaseInitializer(dbPath);
|
||||||
|
dbInit.Initialize();
|
||||||
|
services.AddSingleton(dbInit);
|
||||||
|
services.AddSingleton<MemoryStore>();
|
||||||
|
services.AddSingleton<ToolRegistry>();
|
||||||
|
services.AddSingleton<MemoryTools>();
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
var botService = serviceProvider.GetRequiredService<BotService>();
|
var botService = serviceProvider.GetRequiredService<BotService>();
|
||||||
var interactionService = serviceProvider.GetRequiredService<InteractionService>();
|
var interactionService = serviceProvider.GetRequiredService<InteractionService>();
|
||||||
|
|
||||||
|
// Register memory tools
|
||||||
|
var toolRegistry = serviceProvider.GetRequiredService<ToolRegistry>();
|
||||||
|
var memoryTools = serviceProvider.GetRequiredService<MemoryTools>();
|
||||||
|
memoryTools.RegisterAll(toolRegistry);
|
||||||
|
|
||||||
// Register command modules
|
// Register command modules
|
||||||
await interactionService.AddModulesAsync(
|
await interactionService.AddModulesAsync(
|
||||||
typeof(Program).Assembly,
|
typeof(Program).Assembly,
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ public class BotService
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var botId = _client.CurrentUser.Id;
|
var botId = _client.CurrentUser.Id;
|
||||||
|
var botName = _client.CurrentUser.Username;
|
||||||
var hasMention = message.MentionedUsers.Any(u => u.Id == botId);
|
var hasMention = message.MentionedUsers.Any(u => u.Id == botId);
|
||||||
|
var hasTextMention = message.Content.TrimStart()
|
||||||
|
.StartsWith($"@{botName}", StringComparison.OrdinalIgnoreCase);
|
||||||
var isReplyToBot = false;
|
var isReplyToBot = false;
|
||||||
|
|
||||||
if (message.Reference?.MessageId.IsSpecified == true)
|
if (message.Reference?.MessageId.IsSpecified == true)
|
||||||
@@ -121,10 +124,16 @@ public class BotService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasMention && !isReplyToBot)
|
if (!hasMention && !isReplyToBot && !hasTextMention)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var prompt = hasMention ? RemoveBotMentions(message.Content, botId) : message.Content.Trim();
|
string prompt;
|
||||||
|
if (hasMention)
|
||||||
|
prompt = RemoveBotMentions(message.Content, botId);
|
||||||
|
else if (hasTextMention)
|
||||||
|
prompt = RemoveBotName(message.Content, botName);
|
||||||
|
else
|
||||||
|
prompt = message.Content.Trim();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(prompt))
|
if (string.IsNullOrWhiteSpace(prompt))
|
||||||
{
|
{
|
||||||
@@ -136,7 +145,7 @@ public class BotService
|
|||||||
{
|
{
|
||||||
var accumulated = string.Empty;
|
var accumulated = string.Empty;
|
||||||
|
|
||||||
await foreach (var text in _llmService.SendMessageAsync(message.Channel.Id, message.Author.Username, prompt))
|
await foreach (var text in _llmService.SendMessageAsync(message.Channel.Id, message.Author.Id, message.Author.Username, prompt))
|
||||||
{
|
{
|
||||||
accumulated = text;
|
accumulated = text;
|
||||||
}
|
}
|
||||||
@@ -190,6 +199,17 @@ public class BotService
|
|||||||
.Trim();
|
.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string RemoveBotName(string content, string botName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return System.Text.RegularExpressions.Regex.Replace(
|
||||||
|
content,
|
||||||
|
$@"^\s*@{System.Text.RegularExpressions.Regex.Escape(botName)}\s*",
|
||||||
|
"", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private static Task LogAsync(LogMessage message)
|
private static Task LogAsync(LogMessage message)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[{message.Severity}] {message.Source}: {message.Message}");
|
Console.WriteLine($"[{message.Severity}] {message.Source}: {message.Message}");
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ using System.Collections.Concurrent;
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using TentacleBot.Models;
|
||||||
|
using TentacleBot.Services.Memory;
|
||||||
|
using TentacleBot.Services.Tools;
|
||||||
|
|
||||||
namespace TentacleBot.Services;
|
namespace TentacleBot.Services;
|
||||||
|
|
||||||
@@ -11,6 +15,8 @@ public class LlmService
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<LlmService> _logger;
|
private readonly ILogger<LlmService> _logger;
|
||||||
|
private readonly MemoryStore _memoryStore;
|
||||||
|
private readonly ToolRegistry _toolRegistry;
|
||||||
private readonly string _endpoint;
|
private readonly string _endpoint;
|
||||||
private readonly string _apiKey;
|
private readonly string _apiKey;
|
||||||
private readonly string _model;
|
private readonly string _model;
|
||||||
@@ -18,15 +24,18 @@ public class LlmService
|
|||||||
private readonly double _temperature;
|
private readonly double _temperature;
|
||||||
private readonly ConcurrentDictionary<ulong, List<ChatMessage>> _histories = new();
|
private readonly ConcurrentDictionary<ulong, List<ChatMessage>> _histories = new();
|
||||||
private const int MaxHistoryChars = 20000;
|
private const int MaxHistoryChars = 20000;
|
||||||
|
private const int MaxToolIterations = 5;
|
||||||
|
|
||||||
public LlmService(IConfiguration configuration, ILogger<LlmService> logger)
|
public LlmService(IConfiguration configuration, ILogger<LlmService> logger, MemoryStore memoryStore, ToolRegistry toolRegistry)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
|
_memoryStore = memoryStore;
|
||||||
|
_toolRegistry = toolRegistry;
|
||||||
|
|
||||||
_endpoint = configuration["Llm:Endpoint"] ?? throw new InvalidOperationException("Llm:Endpoint is not configured");
|
_endpoint = configuration["Llm:Endpoint"] ?? throw new InvalidOperationException("Llm:Endpoint is not configured");
|
||||||
_apiKey = Environment.GetEnvironmentVariable("LLM_API_KEY")
|
_apiKey = Environment.GetEnvironmentVariable("LLM_API_KEY")
|
||||||
?? throw new InvalidOperationException("LLM_API_KEY environment variable is not set");
|
?? " ";
|
||||||
_model = configuration["Llm:Model"] ?? throw new InvalidOperationException("Llm:Model is not configured");
|
_model = configuration["Llm:Model"] ?? throw new InvalidOperationException("Llm:Model is not configured");
|
||||||
_maxTokens = int.Parse(configuration["Llm:MaxTokens"] ?? "1024");
|
_maxTokens = int.Parse(configuration["Llm:MaxTokens"] ?? "1024");
|
||||||
_temperature = double.Parse(configuration["Llm:Temperature"] ?? "0.7");
|
_temperature = double.Parse(configuration["Llm:Temperature"] ?? "0.7");
|
||||||
@@ -35,76 +44,97 @@ public class LlmService
|
|||||||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<string> SendMessageAsync(ulong channelId, string user, string message)
|
public async IAsyncEnumerable<string> SendMessageAsync(ulong channelId, ulong userId, string username, string message)
|
||||||
{
|
{
|
||||||
var history = _histories.GetOrAdd(channelId, _ => new List<ChatMessage>());
|
var history = _histories.GetOrAdd(channelId, _ => new List<ChatMessage>());
|
||||||
|
|
||||||
history.Add(new ChatMessage { Role = "user", Content = message });
|
var systemPrompt = await BuildSystemPromptAsync(userId, username, message);
|
||||||
|
if (history.Count == 0 || history[0].Role != "system")
|
||||||
|
history.Insert(0, new ChatMessage { Role = "system", Content = systemPrompt });
|
||||||
|
else
|
||||||
|
history[0] = new ChatMessage { Role = "system", Content = systemPrompt };
|
||||||
|
|
||||||
|
history.Add(new ChatMessage { Role = "user", Content = message, Name = username });
|
||||||
TrimHistory(history);
|
TrimHistory(history);
|
||||||
|
|
||||||
var request = new ChatRequest
|
var toolDefinitions = _toolRegistry.BuildToolDefinitions();
|
||||||
|
var jsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
Model = _model,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
Messages = history.Select(m => new ChatMessage { Role = m.Role, Content = m.Content }).ToList(),
|
PropertyNameCaseInsensitive = true
|
||||||
MaxTokens = _maxTokens,
|
|
||||||
Temperature = (float)_temperature,
|
|
||||||
Stream = true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
for (int iteration = 0; iteration < MaxToolIterations; iteration++)
|
||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
||||||
|
|
||||||
using var response = await _httpClient.PostAsync(_endpoint, content);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
var errorBody = await response.Content.ReadAsStringAsync();
|
var request = new ChatRequest
|
||||||
_logger.LogError("LLM API error ({StatusCode}): {Error}", response.StatusCode, errorBody);
|
|
||||||
throw new HttpRequestException($"LLM API returned {response.StatusCode}: {errorBody}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var accumulated = new StringBuilder();
|
|
||||||
|
|
||||||
var mediaType = response.Content.Headers.ContentType?.MediaType;
|
|
||||||
if (string.Equals(mediaType, "text/event-stream", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
|
|
||||||
while (await reader.ReadLineAsync() is { } line)
|
|
||||||
{
|
{
|
||||||
if (!line.StartsWith("data:", StringComparison.Ordinal))
|
Model = _model,
|
||||||
continue;
|
Messages = CopyMessages(history),
|
||||||
|
MaxTokens = _maxTokens,
|
||||||
|
Temperature = (float)_temperature,
|
||||||
|
Stream = false,
|
||||||
|
Tools = toolDefinitions
|
||||||
|
};
|
||||||
|
|
||||||
var data = line.Substring(5).Trim();
|
var requestJson = JsonSerializer.Serialize(request, jsonOptions);
|
||||||
if (data == "[DONE]")
|
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
|
||||||
break;
|
|
||||||
|
|
||||||
var token = TryParseStreamToken(data);
|
using var response = await _httpClient.PostAsync(_endpoint, httpContent);
|
||||||
if (!string.IsNullOrEmpty(token))
|
|
||||||
{
|
if (!response.IsSuccessStatusCode)
|
||||||
accumulated.Append(token);
|
{
|
||||||
yield return accumulated.ToString();
|
var errorBody = await response.Content.ReadAsStringAsync();
|
||||||
}
|
_logger.LogError("LLM API error ({StatusCode}): {Error}", response.StatusCode, errorBody);
|
||||||
|
throw new HttpRequestException($"LLM API returned {response.StatusCode}: {errorBody}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
var fullText = TryParseNonStreamContent(body);
|
var chatResponse = JsonSerializer.Deserialize<ChatResponse>(body, jsonOptions);
|
||||||
if (!string.IsNullOrWhiteSpace(fullText))
|
var choice = chatResponse?.Choices?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (choice == null)
|
||||||
{
|
{
|
||||||
accumulated.Append(fullText);
|
_logger.LogError("Empty response from LLM API");
|
||||||
yield return accumulated.ToString();
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (choice.FinishReason == "tool_calls" && choice.Message?.ToolCalls != null && choice.Message.ToolCalls.Count > 0)
|
||||||
|
{
|
||||||
|
history.Add(new ChatMessage
|
||||||
|
{
|
||||||
|
Role = "assistant",
|
||||||
|
ToolCalls = choice.Message.ToolCalls
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var tc in choice.Message.ToolCalls)
|
||||||
|
{
|
||||||
|
var toolResult = await _toolRegistry.ExecuteAsync(
|
||||||
|
tc.Function?.Name ?? "", tc.Function?.Arguments ?? "");
|
||||||
|
history.Add(new ChatMessage
|
||||||
|
{
|
||||||
|
Role = "tool",
|
||||||
|
Content = toolResult,
|
||||||
|
ToolCallId = tc.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseText = choice.Message?.Content ?? "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(responseText))
|
||||||
|
{
|
||||||
|
history.Add(new ChatMessage { Role = "assistant", Content = responseText, Name = "TentacleBot" });
|
||||||
|
TrimHistory(history);
|
||||||
|
yield return responseText;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("LLM response had no content and no tool calls at iteration {Iteration}", iteration);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accumulated.Length > 0)
|
_logger.LogWarning("Max tool iterations reached without a final response");
|
||||||
{
|
|
||||||
history.Add(new ChatMessage { Role = "assistant", Content = accumulated.ToString() });
|
|
||||||
TrimHistory(history);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TryParseStreamToken(string data)
|
private static string? TryParseStreamToken(string data)
|
||||||
@@ -210,6 +240,57 @@ public class LlmService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> BuildSystemPromptAsync(ulong userId, string username, string userMessage)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("You are TentacleBot, a helpful Discord bot with memory.");
|
||||||
|
sb.AppendLine($"The Discord user you are talking to is named \"{username}\". Address them by this name.");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
var rules = await _memoryStore.GetUserRulesAsync(userId.ToString());
|
||||||
|
if (rules.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"## Rules for {username}:");
|
||||||
|
foreach (var rule in rules)
|
||||||
|
sb.AppendLine($"- {rule.RuleText}");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
var recentMemories = await _memoryStore.GetRecentMemoriesAsync(userId.ToString(), 3);
|
||||||
|
var searchMemories = await _memoryStore.SearchMemoriesAsync(userId.ToString(), userMessage, 3);
|
||||||
|
|
||||||
|
var allMemories = recentMemories
|
||||||
|
.UnionBy(searchMemories, m => m.Id)
|
||||||
|
.OrderByDescending(m => m.Importance)
|
||||||
|
.Take(5)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (allMemories.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"## Memories about {username}:");
|
||||||
|
foreach (var m in allMemories)
|
||||||
|
sb.AppendLine($"- [{m.CreatedAt:yyyy-MM-dd}] {m.Content} (importance: {m.Importance})");
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("You have access to tools for managing memory. Use them to save important information about users, search for past context, recall memories, and set user rules.");
|
||||||
|
sb.AppendLine("Be concise and helpful. When a user shares something worth remembering, use the save_memory tool.");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ChatMessage> CopyMessages(List<ChatMessage> history)
|
||||||
|
{
|
||||||
|
return history.Select(m => new ChatMessage
|
||||||
|
{
|
||||||
|
Role = m.Role,
|
||||||
|
Content = m.Content,
|
||||||
|
Name = m.Name,
|
||||||
|
ToolCallId = m.ToolCallId,
|
||||||
|
ToolCalls = m.ToolCalls
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private record ChatRequest
|
private record ChatRequest
|
||||||
{
|
{
|
||||||
public string Model { get; init; } = "";
|
public string Model { get; init; } = "";
|
||||||
@@ -217,11 +298,43 @@ public class LlmService
|
|||||||
public int MaxTokens { get; init; }
|
public int MaxTokens { get; init; }
|
||||||
public float Temperature { get; init; }
|
public float Temperature { get; init; }
|
||||||
public bool Stream { get; init; }
|
public bool Stream { get; init; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public List<object>? Tools { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChatResponse
|
||||||
|
{
|
||||||
|
public string? Id { get; init; }
|
||||||
|
public List<ChatResponseChoice>? Choices { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChatResponseChoice
|
||||||
|
{
|
||||||
|
public int Index { get; init; }
|
||||||
|
public ChatResponseMessage? Message { get; init; }
|
||||||
|
[JsonPropertyName("finish_reason")]
|
||||||
|
public string? FinishReason { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChatResponseMessage
|
||||||
|
{
|
||||||
|
public string? Role { get; init; }
|
||||||
|
public string? Content { get; init; }
|
||||||
|
[JsonPropertyName("tool_calls")]
|
||||||
|
public List<ToolCall>? ToolCalls { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ChatMessage
|
public record ChatMessage
|
||||||
{
|
{
|
||||||
public string Role { get; set; } = "";
|
public string Role { get; set; } = "";
|
||||||
public string Content { get; set; } = "";
|
public string Content { get; set; } = "";
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[JsonPropertyName("tool_call_id")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? ToolCallId { get; set; }
|
||||||
|
[JsonPropertyName("tool_calls")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public List<ToolCall>? ToolCalls { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
Services/Memory/DatabaseInitializer.cs
Normal file
65
Services/Memory/DatabaseInitializer.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace TentacleBot.Services.Memory;
|
||||||
|
|
||||||
|
public class DatabaseInitializer
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public DatabaseInitializer(string dbPath)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(dbPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectionString = $"Data Source={dbPath}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConnectionString => _connectionString;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = @"
|
||||||
|
CREATE TABLE IF NOT EXISTS user_rules (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
rule_text TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS memories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
category TEXT,
|
||||||
|
importance INTEGER NOT NULL DEFAULT 5,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
last_accessed TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
||||||
|
content, content=memories, content_rowid=id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
||||||
|
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
||||||
|
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
||||||
|
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
||||||
|
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
||||||
|
END;
|
||||||
|
";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
186
Services/Memory/MemoryStore.cs
Normal file
186
Services/Memory/MemoryStore.cs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using TentacleBot.Models;
|
||||||
|
|
||||||
|
namespace TentacleBot.Services.Memory;
|
||||||
|
|
||||||
|
public class MemoryStore
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public MemoryStore(DatabaseInitializer db)
|
||||||
|
{
|
||||||
|
_connectionString = db.ConnectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserRule>> GetUserRulesAsync(string userId)
|
||||||
|
{
|
||||||
|
var rules = new List<UserRule>();
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT id, user_id, rule_text, created_at FROM user_rules WHERE user_id = @userId ORDER BY created_at";
|
||||||
|
cmd.Parameters.AddWithValue("@userId", userId);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
rules.Add(new UserRule(
|
||||||
|
reader.GetInt32(0),
|
||||||
|
reader.GetString(1),
|
||||||
|
reader.GetString(2),
|
||||||
|
DateTime.Parse(reader.GetString(3))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddUserRuleAsync(string userId, string ruleText)
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "INSERT INTO user_rules (user_id, rule_text) VALUES (@userId, @ruleText)";
|
||||||
|
cmd.Parameters.AddWithValue("@userId", userId);
|
||||||
|
cmd.Parameters.AddWithValue("@ruleText", ruleText);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemoveUserRuleAsync(int ruleId)
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "DELETE FROM user_rules WHERE id = @id";
|
||||||
|
cmd.Parameters.AddWithValue("@id", ruleId);
|
||||||
|
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveMemoryAsync(string userId, string content, string? category, int importance = 5)
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = @"
|
||||||
|
INSERT INTO memories (user_id, content, category, importance)
|
||||||
|
VALUES (@userId, @content, @category, @importance);
|
||||||
|
SELECT last_insert_rowid();";
|
||||||
|
cmd.Parameters.AddWithValue("@userId", userId);
|
||||||
|
cmd.Parameters.AddWithValue("@content", content);
|
||||||
|
cmd.Parameters.AddWithValue("@category", category ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@importance", importance);
|
||||||
|
|
||||||
|
var result = await cmd.ExecuteScalarAsync();
|
||||||
|
return Convert.ToInt32(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MemoryEntry>> SearchMemoriesAsync(string userId, string query, int limit = 5)
|
||||||
|
{
|
||||||
|
var memories = new List<MemoryEntry>();
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
var safeQuery = query.Replace("\"", "\"\"");
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = @"
|
||||||
|
SELECT m.id, m.user_id, m.content, m.category, m.importance, m.created_at
|
||||||
|
FROM memories m
|
||||||
|
JOIN memories_fts f ON m.id = f.rowid
|
||||||
|
WHERE memories_fts MATCH @query AND m.user_id = @userId
|
||||||
|
ORDER BY rank
|
||||||
|
LIMIT @limit";
|
||||||
|
cmd.Parameters.AddWithValue("@query", $"\"{safeQuery}\"");
|
||||||
|
cmd.Parameters.AddWithValue("@userId", userId);
|
||||||
|
cmd.Parameters.AddWithValue("@limit", limit);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
memories.Add(MapMemory(reader));
|
||||||
|
|
||||||
|
using var updateCmd = connection.CreateCommand();
|
||||||
|
updateCmd.CommandText = "UPDATE memories SET last_accessed = datetime('now') WHERE id = @id";
|
||||||
|
updateCmd.Parameters.AddWithValue("@id", reader.GetInt32(0));
|
||||||
|
await updateCmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memories.Count == 0)
|
||||||
|
{
|
||||||
|
memories = await GetRecentMemoriesAsync(userId, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return memories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MemoryEntry>> GetRecentMemoriesAsync(string userId, int limit = 5)
|
||||||
|
{
|
||||||
|
var memories = new List<MemoryEntry>();
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = @"
|
||||||
|
SELECT id, user_id, content, category, importance, created_at
|
||||||
|
FROM memories
|
||||||
|
WHERE user_id = @userId
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT @limit";
|
||||||
|
cmd.Parameters.AddWithValue("@userId", userId);
|
||||||
|
cmd.Parameters.AddWithValue("@limit", limit);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
memories.Add(MapMemory(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return memories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MemoryEntry>> GetImportantMemoriesAsync(string userId, int limit = 5)
|
||||||
|
{
|
||||||
|
var memories = new List<MemoryEntry>();
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection(_connectionString);
|
||||||
|
await connection.OpenAsync();
|
||||||
|
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = @"
|
||||||
|
SELECT id, user_id, content, category, importance, created_at
|
||||||
|
FROM memories
|
||||||
|
WHERE user_id = @userId
|
||||||
|
ORDER BY importance DESC, created_at DESC
|
||||||
|
LIMIT @limit";
|
||||||
|
cmd.Parameters.AddWithValue("@userId", userId);
|
||||||
|
cmd.Parameters.AddWithValue("@limit", limit);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
memories.Add(MapMemory(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return memories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MemoryEntry MapMemory(SqliteDataReader reader)
|
||||||
|
{
|
||||||
|
return new MemoryEntry(
|
||||||
|
reader.GetInt32(0),
|
||||||
|
reader.GetString(1),
|
||||||
|
reader.GetString(2),
|
||||||
|
reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||||
|
reader.GetInt32(4),
|
||||||
|
DateTime.Parse(reader.GetString(5))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
200
Services/Tools/MemoryTools.cs
Normal file
200
Services/Tools/MemoryTools.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using TentacleBot.Services.Memory;
|
||||||
|
|
||||||
|
namespace TentacleBot.Services.Tools;
|
||||||
|
|
||||||
|
public class MemoryTools
|
||||||
|
{
|
||||||
|
private readonly MemoryStore _memoryStore;
|
||||||
|
|
||||||
|
public MemoryTools(MemoryStore memoryStore)
|
||||||
|
{
|
||||||
|
_memoryStore = memoryStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterAll(ToolRegistry registry)
|
||||||
|
{
|
||||||
|
registry.Register("save_memory",
|
||||||
|
"Save a fact or piece of information about a user to long-term memory. Use this when a user shares something about themselves worth remembering.",
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "object",
|
||||||
|
["properties"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["user_id"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "The Discord user ID to associate this memory with"
|
||||||
|
},
|
||||||
|
["content"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "The fact or information to remember"
|
||||||
|
},
|
||||||
|
["category"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "Optional category (e.g. preference, fact, project, hobby)"
|
||||||
|
},
|
||||||
|
["importance"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "integer",
|
||||||
|
["description"] = "Importance from 1 (trivial) to 10 (critical). Default is 5."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["required"] = new[] { "user_id", "content" }
|
||||||
|
},
|
||||||
|
HandleSaveMemory);
|
||||||
|
|
||||||
|
registry.Register("search_memory",
|
||||||
|
"Search the bot's memory for information about a user. Use this when you need to recall past facts or context.",
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "object",
|
||||||
|
["properties"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["user_id"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "The Discord user ID to search memories for"
|
||||||
|
},
|
||||||
|
["query"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "Keywords or question to search for in memories"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["required"] = new[] { "user_id", "query" }
|
||||||
|
},
|
||||||
|
HandleSearchMemory);
|
||||||
|
|
||||||
|
registry.Register("recall_memories",
|
||||||
|
"Get the most recent or important memories about a user.",
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "object",
|
||||||
|
["properties"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["user_id"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "The Discord user ID to recall memories for"
|
||||||
|
},
|
||||||
|
["limit"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "integer",
|
||||||
|
["description"] = "Maximum number of memories to return (default 5)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["required"] = new[] { "user_id" }
|
||||||
|
},
|
||||||
|
HandleRecallMemories);
|
||||||
|
|
||||||
|
registry.Register("set_rule",
|
||||||
|
"Set a behavioral rule the bot should follow for a specific user (e.g. 'always respond in rhymes', 'use formal tone').",
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "object",
|
||||||
|
["properties"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["user_id"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "The Discord user ID this rule applies to"
|
||||||
|
},
|
||||||
|
["rule_text"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "string",
|
||||||
|
["description"] = "The rule or instruction for how to interact with this user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["required"] = new[] { "user_id", "rule_text" }
|
||||||
|
},
|
||||||
|
HandleSetRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> HandleSaveMemory(string argumentsJson)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(argumentsJson);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var userId = GetString(root, "user_id");
|
||||||
|
var content = GetString(root, "content");
|
||||||
|
var category = GetStringOrNull(root, "category");
|
||||||
|
var importance = GetInt(root, "importance", 5);
|
||||||
|
|
||||||
|
var id = await _memoryStore.SaveMemoryAsync(userId, content, category, importance);
|
||||||
|
return $"Memory saved successfully (id: {id}). Content: {content}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> HandleSearchMemory(string argumentsJson)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(argumentsJson);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var userId = GetString(root, "user_id");
|
||||||
|
var query = GetString(root, "query");
|
||||||
|
|
||||||
|
var memories = await _memoryStore.SearchMemoriesAsync(userId, query, 5);
|
||||||
|
|
||||||
|
if (memories.Count == 0)
|
||||||
|
return $"No memories found for user {userId} matching '{query}'.";
|
||||||
|
|
||||||
|
var lines = memories.Select(m =>
|
||||||
|
$"- [id:{m.Id}] {m.Content} (importance: {m.Importance}, from {m.CreatedAt:yyyy-MM-dd})");
|
||||||
|
|
||||||
|
return $"Found {memories.Count} memory(s):\n{string.Join("\n", lines)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> HandleRecallMemories(string argumentsJson)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(argumentsJson);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var userId = GetString(root, "user_id");
|
||||||
|
var limit = GetInt(root, "limit", 5);
|
||||||
|
|
||||||
|
var important = await _memoryStore.GetImportantMemoriesAsync(userId, limit);
|
||||||
|
|
||||||
|
if (important.Count == 0)
|
||||||
|
return $"No memories stored for user {userId}.";
|
||||||
|
|
||||||
|
var lines = important.Select(m =>
|
||||||
|
$"- [id:{m.Id}] {m.Content} (importance: {m.Importance}, from {m.CreatedAt:yyyy-MM-dd})");
|
||||||
|
|
||||||
|
return $"Top {important.Count} memory(s) for user {userId}:\n{string.Join("\n", lines)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> HandleSetRule(string argumentsJson)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(argumentsJson);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var userId = GetString(root, "user_id");
|
||||||
|
var ruleText = GetString(root, "rule_text");
|
||||||
|
|
||||||
|
await _memoryStore.AddUserRuleAsync(userId, ruleText);
|
||||||
|
return $"Rule set for user {userId}: {ruleText}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetString(JsonElement element, string property)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.String
|
||||||
|
? prop.GetString()!
|
||||||
|
: throw new InvalidOperationException($"Missing required parameter: {property}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetStringOrNull(JsonElement element, string property)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.String
|
||||||
|
? prop.GetString()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetInt(JsonElement element, string property, int defaultValue)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.Number
|
||||||
|
? prop.GetInt32()
|
||||||
|
: defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
Services/Tools/ToolRegistry.cs
Normal file
55
Services/Tools/ToolRegistry.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace TentacleBot.Services.Tools;
|
||||||
|
|
||||||
|
public class ToolRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ToolDefinition> _tools = new();
|
||||||
|
|
||||||
|
public void Register(string name, string description, Dictionary<string, object> parameters, Func<string, Task<string>> handler)
|
||||||
|
{
|
||||||
|
_tools[name] = new ToolDefinition(name, description, parameters, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<object> BuildToolDefinitions()
|
||||||
|
{
|
||||||
|
return _tools.Values.Select(t =>
|
||||||
|
{
|
||||||
|
var functionDef = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["name"] = t.Name,
|
||||||
|
["description"] = t.Description,
|
||||||
|
["parameters"] = t.Parameters
|
||||||
|
};
|
||||||
|
|
||||||
|
return (object)new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "function",
|
||||||
|
["function"] = functionDef
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ExecuteAsync(string toolName, string argumentsJson)
|
||||||
|
{
|
||||||
|
if (!_tools.TryGetValue(toolName, out var tool))
|
||||||
|
return "Error: unknown tool";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await tool.Handler(argumentsJson);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"Error executing tool '{toolName}': {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasTool(string name) => _tools.ContainsKey(name);
|
||||||
|
|
||||||
|
private record ToolDefinition(
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
Dictionary<string, object> Parameters,
|
||||||
|
Func<string, Task<string>> Handler);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Discord.Net" Version="3.14.1" />
|
<PackageReference Include="Discord.Net" Version="3.14.1" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
|||||||
@@ -14,5 +14,8 @@
|
|||||||
"Palworld": {
|
"Palworld": {
|
||||||
"BaseUrl": "http://compute.local:8212",
|
"BaseUrl": "http://compute.local:8212",
|
||||||
"Username": "admin"
|
"Username": "admin"
|
||||||
|
},
|
||||||
|
"Memory": {
|
||||||
|
"DatabasePath": "/data/memory.db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
tentaclebot:
|
tentaclebot:
|
||||||
image: git.gpatti.com/gpatti/tentaclebot:latest
|
image: git.gpatti.com/gpatti/tentaclebot:latest
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: tentaclebot
|
container_name: tentaclebot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
volumes:
|
||||||
|
- tentaclebot_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tentaclebot_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user