mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 13:49:41 +00:00
Enabled sending document analysis sessions with attachments in the chat (#627)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Co-authored-by: Thorsten Sommer <mail@tsommer.org>
This commit is contained in:
1 parent
3abc2bf0f1
commit
85af3ed651
7 files changed
+171
-30
No files matched your search
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -11,8 +12,50 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class WorkspaceBehaviour
|
||||
{
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(WorkspaceBehaviour));
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(WorkspaceBehaviour).Namespace, nameof(WorkspaceBehaviour));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Semaphores for synchronizing chat storage operations per chat.
|
||||
/// This prevents race conditions when multiple threads try to write
|
||||
/// the same chat file simultaneously.
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> CHAT_STORAGE_SEMAPHORES = new();
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for acquiring the chat storage semaphore.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6);
|
||||
|
||||
private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId)
|
||||
{
|
||||
var key = $"{workspaceId}_{chatId}";
|
||||
return CHAT_STORAGE_SEMAPHORES.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to acquire the chat storage semaphore within the configured timeout.
|
||||
/// </summary>
|
||||
/// <param name="workspaceId">The workspace ID.</param>
|
||||
/// <param name="chatId">The chat ID.</param>
|
||||
/// <param name="callerName">The name of the calling method for logging purposes.</param>
|
||||
/// <returns>A tuple containing whether the semaphore was acquired and the semaphore instance.</returns>
|
||||
private static async Task<(bool Acquired, SemaphoreSlim Semaphore)> TryAcquireChatSemaphoreAsync(Guid workspaceId, Guid chatId, string callerName)
|
||||
{
|
||||
var semaphore = GetChatSemaphore(workspaceId, chatId);
|
||||
var acquired = await semaphore.WaitAsync(SEMAPHORE_TIMEOUT);
|
||||
|
||||
if (!acquired)
|
||||
LOG.LogWarning("Failed to acquire chat storage semaphore within {Timeout} seconds for workspace '{WorkspaceId}', chat '{ChatId}' in method '{CallerName}'. Skipping operation to prevent potential race conditions or deadlocks.",
|
||||
SEMAPHORE_TIMEOUT.TotalSeconds,
|
||||
workspaceId,
|
||||
chatId,
|
||||
callerName);
|
||||
|
||||
return (acquired, semaphore);
|
||||
}
|
||||
|
||||
public static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
@@ -37,35 +80,52 @@ public static class WorkspaceBehaviour
|
||||
|
||||
public static async Task StoreChat(ChatThread chat)
|
||||
{
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(chatDirectory);
|
||||
|
||||
// Save the chat name:
|
||||
var chatNamePath = Path.Join(chatDirectory, "name");
|
||||
await File.WriteAllTextAsync(chatNamePath, chat.Name);
|
||||
|
||||
// Save the thread as thread.json:
|
||||
var chatPath = Path.Join(chatDirectory, "thread.json");
|
||||
await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8);
|
||||
// Try to acquire the semaphore for this specific chat to prevent concurrent writes to the same file:
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChat));
|
||||
if (!acquired)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(chatDirectory);
|
||||
|
||||
// Save the chat name:
|
||||
var chatNamePath = Path.Join(chatDirectory, "name");
|
||||
await File.WriteAllTextAsync(chatNamePath, chat.Name);
|
||||
|
||||
// Save the thread as thread.json:
|
||||
var chatPath = Path.Join(chatDirectory, "thread.json");
|
||||
await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ChatThread?> LoadChat(LoadChat loadChat)
|
||||
{
|
||||
var chatPath = loadChat.WorkspaceId == Guid.Empty
|
||||
? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString())
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString());
|
||||
|
||||
if(!Directory.Exists(chatPath))
|
||||
// Try to acquire the semaphore for this specific chat to prevent concurrent read/writes to the same file:
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChat));
|
||||
if (!acquired)
|
||||
return null;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var chatPath = loadChat.WorkspaceId == Guid.Empty
|
||||
? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString())
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString());
|
||||
|
||||
if(!Directory.Exists(chatPath))
|
||||
return null;
|
||||
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
|
||||
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
|
||||
return chat;
|
||||
@@ -74,6 +134,10 @@ public static class WorkspaceBehaviour
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> LoadWorkspaceName(Guid workspaceId)
|
||||
@@ -144,7 +208,19 @@ public static class WorkspaceBehaviour
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
Directory.Delete(chatDirectory, true);
|
||||
// Try to acquire the semaphore to prevent deleting while another thread is writing:
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChat));
|
||||
if (!acquired)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(chatDirectory, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName)
|
||||
|
||||
Reference in new issue
Block a user