From 81b900b8871548bf6324b125b296abcc9dc55da6 Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Sat, 3 Oct 2026 18:04:36 +0330 Subject: [PATCH] Last ... --- Documents/Ai Serving.txt | 15 +- Documents/Boards/xDashboard.kanban | 29 +- Documents/Docs/Developer/10.html | 441 ++ Documents/Docs/Developer/11.html | 616 ++ Documents/Docs/Developer/12.html | 454 ++ Documents/Docs/Developer/13.html | 351 + Documents/Docs/Developer/14.html | 682 ++ Documents/Docs/Developer/7_1.html | 658 ++ Documents/Docs/Developer/7_2.html | 737 ++ Documents/Docs/Developer/8.html | 638 ++ Documents/Docs/Developer/9.html | 385 + .../xDashboard-xAiApi-20261003_103219.txt | 5752 +++++++++++++++ .../xDashboard-xAiApi-20261003_114330.txt | 5908 +++++++++++++++ .../xDashboard-xAiApi-20261003_141948.txt | 6283 ++++++++++++++++ .../xDashboard-xAiApi-20261003_144445.txt | 6453 ++++++++++++++++ .../xDashboard-xAiApi-20261003_145153.txt | 6455 +++++++++++++++++ .../xDashboard-xAiApi-20261003_145822.txt | 6453 ++++++++++++++++ .../xDashboard-xAiApi-20261003_172953.txt | 6302 ++++++++++++++++ Modules/xAiService | 2 +- xAiApi | 2 +- 20 files changed, 48612 insertions(+), 4 deletions(-) create mode 100644 Documents/Docs/Developer/10.html create mode 100644 Documents/Docs/Developer/11.html create mode 100644 Documents/Docs/Developer/12.html create mode 100644 Documents/Docs/Developer/13.html create mode 100644 Documents/Docs/Developer/14.html create mode 100644 Documents/Docs/Developer/7_1.html create mode 100644 Documents/Docs/Developer/7_2.html create mode 100644 Documents/Docs/Developer/8.html create mode 100644 Documents/Docs/Developer/9.html create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_103219.txt create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_114330.txt create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_141948.txt create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_144445.txt create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_145153.txt create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_145822.txt create mode 100644 Documents/GeneratedContext/xDashboard-xAiApi-20261003_172953.txt diff --git a/Documents/Ai Serving.txt b/Documents/Ai Serving.txt index d012e77..24abc75 100644 --- a/Documents/Ai Serving.txt +++ b/Documents/Ai Serving.txt @@ -24,4 +24,17 @@ sc delete XLLMService sc delete XLLMEmbdService https://docs.mozilla.ai/llamafile/using-llamafile/api -https://github.com/mozilla-ai/llamafile \ No newline at end of file +https://github.com/mozilla-ai/llamafile + +------------------- + +Uncesored Qwen 3.5 Coder GGUF: + +https://huggingface.co/DavidAU/Qwen3.5-9B-The-Defiant-Fable-Uncensored-Heretic-NEO-IMATRIX-MAX-MTP-GGUF + +------------------- + +Download these files and put them inside xAiApi/Models: + +https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-base.bin +https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-small.bin \ No newline at end of file diff --git a/Documents/Boards/xDashboard.kanban b/Documents/Boards/xDashboard.kanban index da17f1d..b859fd7 100644 --- a/Documents/Boards/xDashboard.kanban +++ b/Documents/Boards/xDashboard.kanban @@ -514,6 +514,33 @@ "id": "171f43f3-ae73-49b7-9239-9f3fb695cc2b", "title": "InProgress", "cards": [ + { + "id": "9757ac0e-2aff-44a1-a0db-7ee4468def1b", + "listId": "171f43f3-ae73-49b7-9239-9f3fb695cc2b", + "title": "Detect a Very Good GGUF Model for Performing OCR on Bynary Contents ...", + "description": "", + "labels": [], + "checkboxes": [], + "comments": [] + }, + { + "id": "5cccaa60-c985-4a99-93eb-adb38a1f79db", + "listId": "171f43f3-ae73-49b7-9239-9f3fb695cc2b", + "title": "Complete OCR Preparation ...", + "description": "", + "labels": [], + "checkboxes": [], + "comments": [] + }, + { + "id": "38a94972-d86a-49a3-8917-9b2d958df660", + "listId": "171f43f3-ae73-49b7-9239-9f3fb695cc2b", + "title": "Complete Audio Transcript ...", + "description": "", + "labels": [], + "checkboxes": [], + "comments": [] + }, { "id": "5d103817-846f-477f-a381-50e86c3cc7d4", "listId": "171f43f3-ae73-49b7-9239-9f3fb695cc2b", @@ -569,7 +596,7 @@ { "id": "07602ca6-38b8-4d19-b0f6-8730e11de1df", "title": "Add Support for Files Attachment in AI Asking ", - "checked": false + "checked": true }, { "id": "149c4343-16ab-4ac8-899b-23d202000b85", diff --git a/Documents/Docs/Developer/10.html b/Documents/Docs/Developer/10.html new file mode 100644 index 0000000..5eca41c --- /dev/null +++ b/Documents/Docs/Developer/10.html @@ -0,0 +1,441 @@ + + + + + +رفع خطای وابستگی چرخشی (Circular Dependency) در سرویس OCR - فن آوران ساحر علم + + + +
+ +
+

🔗 رفع خطای وابستگی چرخشی (Circular Dependency)

+
تحلیل و رفع چرخه وابستگی بین IXFileContentExtractor و IXDefaultAIOCRService
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 گام ۱: تحلیل ریشه خطا

+

پیغام خطای DI به وضوح یک وابستگی چرخشی (Circular Dependency) را نشان می‌دهد:

+
+ IXFileContentExtractor ➔ XVisionFileContentExtractor ➔ IXDefaultAIOCRService ➔ XDefaultAIOCRService ➔ XAIServiceBase ➔ IXFileContentExtractor +
+

چرا این اتفاق افتاد؟

+
    +
  • کلاس XVisionFileContentExtractor برای انجام OCR به IXDefaultAIOCRService نیاز دارد.
  • +
  • کلاس XDefaultAIOCRService از XAIServiceBase ارث‌بری کرده است.
  • +
  • سازنده (Constructor) کلاس XAIServiceBase به IXFileContentExtractor نیاز دارد.
  • +
+

این زنجیره باعث می‌شود کانتینر DI در یک حلقه بی‌نهایت گیر کند و نتواند هیچ‌یک از این سرویس‌ها را مقداردهی اولیه نماید.

+
+ + +
+

💡 گام ۲: استراتژی رفع خطا

+

برای شکستن این چرخه، باید وابستگی XDefaultAIOCRService به XAIServiceBase را حذف کنیم.

+

سرویس OCR فقط نیاز به برقراری ارتباط با مدل زبانی (LLM) برای استخراج متن از تصویر دارد و به هیچ‌وجه به IXFileContentExtractor، IXFileProvider یا IXAiDataProvider نیاز ندارد. بنابراین، با حذف ارث‌بری از XAIServiceBase و پیاده‌سازی مستقیم منطق ساخت IChatClient درون همین کلاس، چرخه وابستگی کاملاً شکسته می‌شود.

+
+ + +
+

🛠️ گام ۳: اصلاح Interface سرویس OCR

+

ابتدا باید ارث‌بری غیرضروری IXAiServiceBase را از اینترفیس حذف کنیم، زیرا این سرویس فقط وظیفه OCR را بر عهده دارد و نیازی به متدهای مدیریت پروژه، مکالمه و پیام ندارد.

+ +
+ MODIFY + xAiApi/Interfaces/IXDefaultAIOCRService.cs +
+ +
using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using System.Collections.Generic;
+
+namespace xAiApi.Interfaces
+{
+    /// <summary>
+    /// سرویس اختصاصی برای انجام OCR بر روی محتوای تصویری ...
+    /// </summary>
+    public interface IXDefaultAIOCRService
+    {
+        /// <summary>
+        /// درخواست انجام OCR بر روی محتوای داده شده ...
+        /// </summary>
+        Task<string> RequestOCRAsync(
+            IList<ChatMessage> messages,
+            CancellationToken cancellationToken = default
+        );
+
+        /// <summary>
+        /// درخواست انجام OCR بر روی محتوای داده شده به صورت Stream ...
+        /// </summary>
+        IAsyncEnumerable<string> RequestOCRAsEnumerable(
+            IList<ChatMessage> messages,
+            CancellationToken cancellationToken = default
+        );
+    }
+}
+
+ + +
+

⚙️ گام ۴: بازنویسی مستقل کلاس XDefaultAIOCRService

+

اکنون کلاس پیاده‌سازی را بازنویسی می‌کنیم تا دیگر از XAIServiceBase ارث‌بری نکند. منطق ساخت IChatClient (که قبلاً در کلاس پایه بود) به صورت مستقیم و تمیز درون این کلاس قرار می‌گیرد.

+ +
+ MODIFY + xAiApi/Providers/XDefaultAIOCRService.cs +
+ +
using System;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using OpenAI;
+using OllamaSharp;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using xAiApi.Configurations;
+using xAiApi.Constants;
+using xAiApi.Extensions;
+using xAiModels.Constants;
+using xAiModels.Extensions;
+using xCommons.Extensions;
+using xExceptions.Constants;
+
+namespace xAiApi.Providers
+{
+    /// <summary>
+    /// پیاده‌سازی مستقل سرویس OCR بدون وابستگی به XAIServiceBase ...
+    /// </summary>
+    public class XDefaultAIOCRService : IXDefaultAIOCRService
+    {
+        private readonly string prompt;
+        private readonly XAiModelDescriptor descriptor;
+        private readonly ILogger<XDefaultAIOCRService> logger;
+
+        public XDefaultAIOCRService(
+            ILogger<XDefaultAIOCRService> logger,
+            XAiApiConfiguration configuration,
+            string model = null,
+            string prompt = null
+        )
+        {
+            this.logger = logger;
+
+            // ۱. دریافت پیکربندی مدل
+            if (model.IsNullOrEmpty())
+            {
+                model = XAiApiConstants.XAiDefaultVisionModelName;
+            }
+
+            descriptor = configuration.GetModel(model);
+            if (descriptor.IsNullOrDefault())
+            {
+                XException.InvalidData.Throw("Invalid OCR Model Configuration");
+            }
+
+            // ۲. دریافت پرامپت استخراج
+            if (prompt.IsNullOrEmpty())
+            {
+                prompt = configuration.GetPrompt(
+                    name: XAiApiConstants.XAiApiContentExtractionPromptName,
+                    @params: null
+                );
+            }
+
+            this.prompt = prompt;
+            if (this.prompt.IsNullOrEmpty())
+            {
+                XException.InvalidArgs.Throw("OCR Prompt cannot be empty");
+            }
+        }
+
+        public virtual async Task<string> RequestOCRAsync(
+            IList<ChatMessage> messages,
+            CancellationToken cancellationToken = default
+        )
+        {
+            if (!messages.HasChild())
+            {
+                XException.InvalidArgs.Throw();
+            }
+
+            using var client = GetClient();
+
+            var pMessage = new ChatMessage(ChatRole.System, prompt);
+            messages = [pMessage, .. messages];
+
+            var response = await client.GetResponseAsync(
+                messages: messages,
+                cancellationToken: cancellationToken
+            );
+
+            if (!response.IsValid())
+            {
+                XException.ActionFailed.Throw();
+            }
+
+            return response.Text;
+        }
+
+        public virtual async IAsyncEnumerable<string> RequestOCRAsEnumerable(
+            IList<ChatMessage> messages,
+            [EnumeratorCancellation] CancellationToken cancellationToken = default
+        )
+        {
+            if (!messages.HasChild())
+            {
+                XException.InvalidArgs.Throw();
+            }
+
+            using var client = GetClient();
+
+            var pMessage = new ChatMessage(ChatRole.System, prompt);
+            messages = [pMessage, .. messages];
+
+            var enumerable = client.GetStreamingResponseAsync(
+                options: null,
+                messages: messages
+            );
+
+            await foreach (var res in enumerable)
+            {
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    yield break;
+                }
+                yield return res.Text;
+            }
+        }
+
+        /// <summary>
+        /// ساخت کلاینت ارتباط با LLM (جایگزین متد GetClient در XAIServiceBase) ...
+        /// </summary>
+        private IChatClient GetClient()
+        {
+            var model = descriptor.LLM;
+            var apiKey = descriptor.ApiKey;
+            var url = new Uri(descriptor.Url);
+            var httpClient = new HttpClient { BaseAddress = url, Timeout = Timeout.InfiniteTimeSpan };
+
+            IChatClient result = null;
+            switch (descriptor.Provider)
+            {
+                case XAiModelProviderType.Ollama:
+                    var ollamaClient = new OllamaApiClient(httpClient, model);
+                    result = new ChatClientBuilder(ollamaClient).UseFunctionInvocation().Build();
+                    break;
+                case XAiModelProviderType.OpenAI:
+                    var openAiClient = new OpenAIClient(
+                        new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey),
+                        new OpenAIClientOptions
+                        {
+                            Endpoint = url,
+                            Transport = new HttpClientPipelineTransport(httpClient)
+                        }
+                    );
+                    result = new ChatClientBuilder(openAiClient.GetChatClient(model).AsIChatClient()).UseFunctionInvocation().Build();
+                    break;
+                default:
+                    XException.InvalidData.Throw($"Unsupported provider: {descriptor.Provider}");
+                    break;
+            }
+
+            if (result == null)
+            {
+                XException.InvalidData.Throw("Failed to create Chat Client");
+            }
+
+            return result;
+        }
+    }
+}
+
+ + +
+

🔌 گام ۵: بررسی ثبت در Dependency Injection

+

با توجه به تغییرات فوق، ثبت سرویس در فایل Startup.cs بدون هیچ تغییری به درستی کار خواهد کرد، زیرا امضای Constructor اکنون ساده‌تر شده و فقط به ILogger و XAiApiConfiguration وابسته است که هر دو از قبل در DI ثبت شده‌اند.

+ +
+ ✅ تأییدیه:
+ خط services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>(); در Startup.cs کاملاً معتبر است و دیگر باعث ایجاد چرخه وابستگی نمی‌شود. +
+ +

خلاصه دستاوردهای این اصلاح:

+ + + + + + + + + + + + + + + + + + + + + +
مزیتتوضیح
🚫 حذف Circular Dependencyچرخه معیوب بین Extractor و سرویس OCR کاملاً شکسته شد.
⚡ افزایش عملکرد (Performance)سرویس OCR دیگر بار اضافی مقداردهی اولیه DataProvider و FileProvider را تحمل نمی‌کند.
🎯 اصل تک‌وظیفه‌ای (SRP)کلاس XDefaultAIOCRService اکنون فقط و فقط مسئول ارتباط با مدل برای OCR است.
🧪 قابلیت تست‌پذیری (Testability)تزریق وابستگی‌های کمتر، نوشتن Unit Test برای این سرویس را بسیار ساده‌تر می‌کند.
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/11.html b/Documents/Docs/Developer/11.html new file mode 100644 index 0000000..edf58e5 --- /dev/null +++ b/Documents/Docs/Developer/11.html @@ -0,0 +1,616 @@ + + + + + +رفع خطای Circular Dependency در XDefaultAIOCRService - فن آوران ساحر علم + + + +
+ +
+

🔗 رفع خطای Circular Dependency

+
تحلیل و رفع حلقه وابستگی بین XVisionFileContentExtractor و XDefaultAIOCRService
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 گام ۱: تحلیل دقیق ریشه خطا

+

پیغام خطای DI به وضوح یک حلقه وابستگی (Circular Dependency) را نشان می‌دهد:

+ +
+ IXFileContentExtractor + → + XVisionFileContentExtractor + → + IXDefaultAIOCRService + → + XDefaultAIOCRService + → + IXFileContentExtractor ❌ +
+ +
+ ⚠️ تحلیل زنجیره: +
    +
  1. XVisionFileContentExtractor در constructor خود به IXDefaultAIOCRService نیاز دارد.
  2. +
  3. XDefaultAIOCRService در constructor خود به IXFileContentExtractor نیاز دارد.
  4. +
  5. اما IXFileContentExtractor همان کامپوزیتی است که شامل XVisionFileContentExtractor می‌باشد!
  6. +
  7. این یک حلقه بی‌نهایت ایجاد می‌کند و DI Container قادر به ساخت هیچ‌یک از این سرویس‌ها نیست.
  8. +
+
+
+ + +
+

🎯 گام ۲: شناسایی علت اصلی

+

با بررسی دقیق کد XDefaultAIOCRService، یک نکته کلیدی کشف شد:

+ +
+
+

❌ پارامتر غیرضروری در Constructor

+

در constructor کلاس XDefaultAIOCRService، پارامتر IXFileContentExtractor fileContentExtractor دریافت می‌شود، اما در هیچ جای کلاس از آن استفاده نمی‌شود!

+
+
+

✅ سرویس OCR مستقل است

+

سرویس OCR فقط نیاز به ارتباط با مدل زبانی (LLM) دارد و به IXFileContentExtractor نیازی ندارد. این پارامتر به اشتباه از نسخه قبلی باقی مانده است.

+
+
+ +

کد مشکل‌دار فعلی:

+
public XDefaultAIOCRService(
+    XAiApiConfiguration configuration,
+    ILogger<XDefaultAIOCRService> logger,
+    IXFileContentExtractor fileContentExtractor,  // ❌ پارامتر غیرضروری
+    string model = null,
+    string prompt = null
+)
+{
+    // ... هیچ استفاده‌ای از fileContentExtractor در body کلاس نیست
+}
+
+ + +
+

💡 گام ۳: راه‌حل اصلاحی

+

برای شکستن حلقه وابستگی، کافی است پارامتر غیرضروری IXFileContentExtractor را از constructor کلاس XDefaultAIOCRService حذف کنیم.

+ +
+ IXFileContentExtractor + → + XVisionFileContentExtractor + → + IXDefaultAIOCRService + → + XDefaultAIOCRService ✅ +
+ +
+ ✅ نتیجه: با حذف این پارامتر، زنجیره وابستگی کاملاً شکسته می‌شود و DI Container می‌تواند تمام سرویس‌ها را بدون مشکل مقداردهی اولیه کند. +
+
+ + +
+

🛠️ گام ۴: اصلاح XDefaultAIOCRService

+ +
+ MODIFY + xAiApi/Providers/XDefaultAIOCRService.cs +
+ +

کد اصلاح‌شده:

+
using System;
+using OpenAI;
+using OllamaSharp;
+using System.Net.Http;
+using System.Threading;
+using xAiModels.Models;
+using xAiApi.Constants;
+using xAiApi.Interfaces;
+using xAiApi.Extensions;
+using System.ClientModel;
+using xCommons.Extensions;
+using xAiModels.Extensions;
+using xAiApi.Configurations;
+using xExceptions.Constants;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using System.ClientModel.Primitives;
+using System.Runtime.CompilerServices;
+
+namespace xAiApi.Providers
+{
+    public class XDefaultAIOCRService : IXDefaultAIOCRService
+    {
+        /// <summary>
+        /// Chat Options ...
+        /// </summary>
+        public ChatOptions Options { get; }
+
+        /// <summary>
+        /// Prompt ...
+        /// </summary>
+        public string Prompt { get; }
+
+        /// <summary>
+        /// Descriptor of Models which used in Service ...
+        /// </summary>
+        public XAiModelDescriptor Descriptor { get; }
+
+        public XDefaultAIOCRService(
+            XAiApiConfiguration configuration,
+            ILogger<XDefaultAIOCRService> logger,
+            // ✅ پارامتر IXFileContentExtractor حذف شد
+            string model = null,
+            string prompt = null
+        )
+        {
+            //
+            // Prepare Model Descriptor ...
+            Descriptor = configuration.GetModel(model);
+            if (!Descriptor.IsValid())
+            {
+                XException.InvalidConfiguration.Throw();
+            }
+            //
+            Prompt = prompt;
+            if (Prompt.IsNullOrEmpty())
+            {
+                XException.InvalidArgs.Throw();
+            }
+            //
+            // Prepare Chat Options based On Tools ...
+            Options = new ChatOptions();
+        }
+
+        //
+        #region OCR ...
+        /// <summary>
+        /// Request for Doing OCR on Given Data Contents ...
+        /// </summary>
+        public virtual async Task<string> RequestOCRAsync(
+            IList<ChatMessage> messages,
+            CancellationToken cancellationToken = default
+        )
+        {
+            //
+            // Validate ...
+            if (!messages.HasChild())
+            {
+                XException.InvalidArgs.Throw();
+            }
+            //
+            using var client = GetClient();
+            //
+            // Preparing Extraction Prompt Message ...
+            var pMessage = new ChatMessage(
+                ChatRole.System,
+                Prompt
+            );
+            //
+            messages = [pMessage, .. messages];
+            //
+            var response = await client.GetResponseAsync(
+                messages: messages,
+                cancellationToken: cancellationToken
+            );
+            //
+            // Validate Response ...
+            if (!response.IsValid())
+            {
+                //
+                // Dispose Client ...
+                client.Dispose();
+                XException.ActionFailed.Throw();
+            }
+            //
+            // Retrieve Response Text ...
+            var result = response.Text;
+            //
+            return result;
+        }
+
+        /// <summary>
+        /// Request for Doing OCR on Given Data Contents as Stream ...
+        /// </summary>
+        public virtual async IAsyncEnumerable<string> RequestOCRAsEnumerable(
+            IList<ChatMessage> messages,
+            [EnumeratorCancellation]
+            CancellationToken cancellationToken = default
+        )
+        {
+            //
+            // Validate ...
+            if (!messages.HasChild())
+            {
+                XException.InvalidArgs.Throw();
+            }
+            //
+            using var client = GetClient();
+            //
+            // Preparing Extraction Prompt Message ...
+            var pMessage = new ChatMessage(
+                ChatRole.System,
+                Prompt
+            );
+            //
+            messages = [pMessage, .. messages];
+            //
+            var enumerable = client.GetStreamingResponseAsync(
+                options: null,
+                messages: messages
+            );
+            //
+            await foreach (var res in enumerable)
+            {
+                //
+                // Cancellation Token ...
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    yield break;
+                }
+                //
+                yield return res.Text;
+            }
+        }
+        #endregion
+
+        //
+        #region Preaprations ...
+        /// <summary>
+        /// Get LLM Client instance for Communicating with LLM ...
+        /// </summary>
+        public virtual IChatClient GetClient()
+        {
+            //
+            // Try to Initialize LLm ...
+            var model = Descriptor.LLM;
+            var apiKey = Descriptor.ApiKey;
+            var url = new Uri(Descriptor.Url);
+            var httpClient = GetHttpClient(Descriptor.Url);
+            //
+            IChatClient result = null;
+            switch (Descriptor.Provider)
+            {
+                //
+                case XAiModelProviderType.Ollama:
+                    //
+                    var ollamaClient = new OllamaApiClient(httpClient, model);
+                    result =
+                        new ChatClientBuilder(ollamaClient)
+                        .UseFunctionInvocation()
+                        .Build();
+                    break;
+                //
+                case XAiModelProviderType.OpenAI:
+                    //
+                    var openAiClient = new OpenAIClient(
+                        new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey),
+                        new OpenAIClientOptions
+                        {
+                            Endpoint = url,
+                            Transport = new HttpClientPipelineTransport(httpClient)
+                        }
+                    );
+                    result =
+                        new ChatClientBuilder(
+                            openAiClient
+                            .GetChatClient(model)
+                            .AsIChatClient())
+                        .UseFunctionInvocation()
+                        .Build();
+                    break;
+                //
+                case XAiModelProviderType.DeepSeek:
+                    break;
+                //
+                case XAiModelProviderType.HuggingFace:
+                    break;
+                //
+                default:
+                    break;
+            }
+            //
+            if (result.IsNullOrDefault())
+            {
+                XException.InvalidData.Throw();
+            }
+            //
+            return result;
+        }
+
+        /// <summary>
+        /// Create Custom HttpClient for Communicating with LLM API ...
+        /// </summary>
+        public virtual HttpClient GetHttpClient(string url = null)
+        {
+            //
+            var result = new HttpClient
+            {
+                //
+                // Disable timeout completely (not recommended for production)
+                Timeout = Timeout.InfiniteTimeSpan,
+                //
+                BaseAddress = url.IsNullOrEmpty()
+                    ? null
+                    : new Uri(url),
+            };
+            //
+            return result;
+        }
+        #endregion
+    }
+}
+
+ + +
+

🔌 گام ۵: بررسی Startup.cs

+

با توجه به تغییرات فوق، ثبت سرویس‌ها در Startup.cs بدون هیچ تغییری به درستی کار خواهد کرد. اما برای اطمینان، ترتیب ثبت سرویس‌ها را بررسی می‌کنیم:

+ +
+ CHECK + xAiApi/Startup.cs (متد ConfigureServices) +
+ +
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (ثبت‌های قبلی)
+
+    // ✅ ثبت File Content Extractors (به ترتیب صحیح)
+    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
+    
+    // ✅ ثبت Vision Extractor (وابسته به IXDefaultAIOCRService)
+    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();
+    
+    // ✅ ثبت کامپوزیت اصلی (لیست بالا را در Constructor دریافت می‌کند)
+    services.AddSingleton<IXFileContentExtractor, XFileContentExtractor>();
+
+    // ✅ ثبت سرویس OCR (دیگر به IXFileContentExtractor وابسته نیست)
+    services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
+
+    // ✅ ثبت سایر سرویس‌های AI
+    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
+    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
+    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
+}
+ +
+ 💡 نکته مهم: ترتیب ثبت سرویس‌ها در DI Container اهمیت ندارد، زیرا .NET Core DI Container به صورت خودکار وابستگی‌ها را حل می‌کند. مهم این است که تمام سرویس‌های مورد نیاز ثبت شده باشند. +
+
+ + +
+

✅ گام ۶: تأیید رفع خطا

+ +

خلاصه تغییرات:

+ + + + + + + + + + + + + + + + +
فایلتغییردلیل
XDefaultAIOCRService.csREMOVE حذف پارامتر IXFileContentExtractorپارامتر غیرضروری بود و باعث Circular Dependency می‌شد
Startup.csNO CHANGE بدون تغییرثبت سرویس‌ها به درستی انجام شده است
+ +

مزایای این اصلاح:

+
+
+

🚫 حذف Circular Dependency

+
    +
  • حلقه وابستگی کاملاً شکسته شد
  • +
  • DI Container می‌تواند تمام سرویس‌ها را بسازد
  • +
+
+
+

⚡ افزایش Performance

+
    +
  • سرویس OCR دیگر بار اضافی ندارد
  • +
  • تزریق وابستگی‌های کمتر = ساخت سریع‌تر
  • +
+
+
+

🎯 اصل تک‌وظیفه‌ای (SRP)

+
    +
  • سرویس OCR فقط مسئول ارتباط با LLM است
  • +
  • وابستگی‌های غیرضروری حذف شدند
  • +
+
+
+

🧪 قابلیت تست‌پذیری

+
    +
  • Unit Test ساده‌تر شد
  • +
  • Mock کردن وابستگی‌ها آسان‌تر است
  • +
+
+
+ +
+ ✅ نتیجه نهایی: +

با حذف پارامتر غیرضروری IXFileContentExtractor از constructor کلاس XDefaultAIOCRService، خطای Circular Dependency کاملاً رفع می‌شود و پروژه بدون مشکل اجرا خواهد شد.

+

پس از اعمال این تغییر، پروژه را rebuild کنید و خطای System.AggregateException دیگر ظاهر نخواهد شد.

+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/12.html b/Documents/Docs/Developer/12.html new file mode 100644 index 0000000..6d5446a --- /dev/null +++ b/Documents/Docs/Developer/12.html @@ -0,0 +1,454 @@ + + + + + +رفع خطای Scoped/Singleton Mismatch در DI - فن آوران ساحر علم + + + +
+ +
+

🔗 رفع خطای Scoped/Singleton Mismatch در DI

+
تحلیل و رفع عدم تطابق طول عمر سرویس‌ها در کانتینر Dependency Injection
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 گام ۱: تحلیل دقیق ریشه خطا

+

پیغام خطای DI به یک مشکل کلاسیک در مدیریت طول عمر سرویس‌ها اشاره می‌کند:

+ +
+ ⚠️ پیغام خطا:
+ Cannot consume scoped service 'IXDefaultAIOCRService' from singleton 'IXFileContentExtractor' +
+ +

قانون طلایی DI در .NET Core:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
قانونتوضیح
✅ مجازSingleton می‌تواند Singleton را مصرف کند
✅ مجازScoped می‌تواند Singleton را مصرف کند
✅ مجازTransient می‌تواند هر چیزی را مصرف کند
❌ ممنوعSingleton نمی‌تواند Scoped را مصرف کند (خطای فعلی)
❌ ممنوعSingleton نمی‌تواند Transient را مصرف کند
+ +
+ 💡 دلیل منطقی: اگر یک Singleton بتواند یک Scoped service را مصرف کند، آن Scoped service عملاً به یک Singleton تبدیل می‌شود (چون فقط یک بار ساخته شده و در تمام درخواست‌ها استفاده می‌شود). این مسئله باعث نشت حافظه و رفتار غیرقابل پیش‌بینی می‌شود. +
+
+ + +
+

🔗 گام ۲: زنجیره وابستگی مشکل‌ساز

+ +

وضعیت فعلی (قبل از اصلاح):

+
+ XFileContentExtractor
(Singleton)
+ → + XVisionFileContentExtractor
(Singleton)
+ → + IXDefaultAIOCRService
(Scoped) ❌
+
+ +
+
+
+ Singleton +
+
+
+ Scoped +
+
+
+ خطا +
+
+ +

بررسی وابستگی‌های XDefaultAIOCRService:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
وابستگینوع ثبت فعلیآیا Stateful است؟
XAiApiConfigurationSingleton❌ خیر (فقط خواندنی)
ILogger<XDefaultAIOCRService>Singleton❌ خیر (Thread-safe)
string modelPrimitive❌ خیر
string promptPrimitive❌ خیر
+ +
+ ✅ نتیجه‌گیری کلیدی: کلاس XDefaultAIOCRService هیچ وابستگی Scoped یا Stateful ندارد و کاملاً Stateless است. بنابراین می‌تواند با خیال راحت به Singleton ارتقا یابد. +
+
+ + +
+

💡 گام ۳: راه‌حل اصلاحی

+

تنها یک تغییر کوچک در فایل Startup.cs لازم است: تغییر طول عمر IXDefaultAIOCRService از Scoped به Singleton.

+ +

زنجیره اصلاح‌شده (بعد از تغییر):

+
+ XFileContentExtractor
(Singleton)
+ → + XVisionFileContentExtractor
(Singleton)
+ → + IXDefaultAIOCRService
(Singleton) ✅
+
+ +
+ 💡 چرا این راه‌حل امن است؟ +
    +
  • ✅ XDefaultAIOCRService هیچ State ای ندارد که بین درخواست‌ها به اشتراک گذاشته شود
  • +
  • ✅ IChatClient در هر فراخوانی GetClient() به صورت محلی ساخته و Dispose می‌شود
  • +
  • ✅ HttpClient با Timeout.InfiniteTimeSpan thread-safe است
  • +
  • ✅ ILogger ذاتاً thread-safe است
  • +
  • ✅ XAiApiConfiguration فقط خواندنی و thread-safe است
  • +
+
+
+ + +
+

🛠️ گام ۴: اصلاح Startup.cs

+ +
+ MODIFY + xAiApi/Startup.cs (متد ConfigureServices) +
+ +

کد اصلاح‌شده:

+
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (ثبت‌های قبلی)
+
+    // ==========================================================
+    // ✅ ثبت File Content Extractors (همگی Singleton)
+    // ==========================================================
+    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
+    
+    // ✅ کامپوزیت اصلی (لیست بالا را در Constructor دریافت می‌کند)
+    services.AddSingleton<IXFileContentExtractor, XFileContentExtractor>();
+
+    // ==========================================================
+    // ✅ ثبت سرویس‌های AI
+    // ==========================================================
+    
+    // ✅ تغییر از AddScoped به AddSingleton (رفع خطای DI)
+    services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
+    services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();
+    
+    // سایر سرویس‌های AI (Scoped باقی می‌مانند چون به DataProvider وابسته‌اند)
+    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
+    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
+    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
+}
+ +
+ ✅ نکته مهم: فقط IXDefaultAIOCRService به Singleton تغییر می‌کند. سایر سرویس‌های AI (IXDefaultAiService, IXDefaultEmbeddingService, IXDefaultThinkingAiService) به دلیل وابستگی به IXAiDataProvider و IXFileProvider که Scoped هستند، باید Scoped باقی بمانند. +
+
+ + +
+

🔒 گام ۵: بررسی Thread Safety

+

با تبدیل XDefaultAIOCRService به Singleton، باید اطمینان حاصل کنیم که این کلاس Thread-safe است:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
بخش از کدوضعیت Thread Safetyتوضیح
Descriptor (Property)✅ Safeفقط در Constructor مقداردهی می‌شود و readonly است
Prompt (Property)✅ Safeفقط در Constructor مقداردهی می‌شود و readonly است
Options (Property)✅ Safeفقط در Constructor مقداردهی می‌شود
GetClient()✅ Safeدر هر فراخوانی یک IChatClient جدید می‌سازد (بدون State مشترک)
GetHttpClient()✅ Safeدر هر فراخوانی یک HttpClient جدید می‌سازد
RequestOCRAsync()✅ Safeاز using var client استفاده می‌کند و State محلی دارد
RequestOCRAsEnumerable()✅ Safeاز using var client استفاده می‌کند و State محلی دارد
+ +
+ ✅ نتیجه: کلاس XDefaultAIOCRService کاملاً Thread-safe است و می‌تواند با اطمینان کامل به عنوان Singleton ثبت شود. +
+
+ + +
+

✅ گام ۶: تأیید رفع کامل خطا

+ +

خلاصه تغییرات:

+ + + + + + + + + + + +
فایلتغییردلیل
Startup.csAddScoped → AddSingleton
برای IXDefaultAIOCRService
رفع خطای Scoped/Singleton Mismatch
+ +

چک‌لیست نهایی:

+
+
    +
  1. ✅ خطای Cannot consume scoped service from singleton رفع می‌شود
  2. +
  3. ✅ تمام Extractor ها (Singleton) می‌توانند XVisionFileContentExtractor را مصرف کنند
  4. +
  5. ✅ XVisionFileContentExtractor می‌تواند IXDefaultAIOCRService را مصرف کند
  6. +
  7. ✅ XFileContentExtractor (Composite) می‌تواند تمام Extractor ها را در Constructor دریافت کند
  8. +
  9. ✅ Thread Safety کاملاً حفظ شده است
  10. +
  11. ✅ Performance بهبود می‌یابد (ساخت یکبار به جای ساخت در هر Request)
  12. +
+
+ +

دستورالعمل اجرا:

+
# ۱. فایل Startup.cs را باز کنید
+# ۲. خط زیر را پیدا کنید:
+services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
+
+# ۳. آن را به این صورت تغییر دهید:
+services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();
+
+# ۴. پروژه را Rebuild کنید
+dotnet build
+
+# ۵. پروژه را اجرا کنید - خطای AggregateException دیگر ظاهر نخواهد شد
+ +
+ 💡 نکته تکمیلی: پس از اعمال این تغییر، می‌توانید با خیال راحت endpoint ExtractContent را در Controller پیاده‌سازی کنید، زیرا تمام زیرساخت DI اکنون به درستی پیکربندی شده است. +
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/13.html b/Documents/Docs/Developer/13.html new file mode 100644 index 0000000..4a9d31a --- /dev/null +++ b/Documents/Docs/Developer/13.html @@ -0,0 +1,351 @@ + + + + + +رفع خطای Circular Dependency در XFileContentExtractor - فن آوران ساحر علم + + + +
+ +
+

🔗 رفع خطای Circular Dependency در XFileContentExtractor

+
تحلیل و رفع حلقه وابستگی در Composite Extractor Pattern
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + +
+

🔍 تحلیل ریشه خطا

+

خطای Circular Dependency به دلیل ثبت نادرست XFileContentExtractor در DI Container ایجاد شده است:

+ +
+ ⚠️ پیغام خطا:
+ A circular dependency was detected for the service of type 'IEnumerable<IXFileContentExtractor>' +
+ +

زنجیره وابستگی مشکل‌ساز:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
مرحلهسرویسوابستگی
۱XFileContentExtractorنیاز به IEnumerable<IXFileContentExtractor> در constructor
۲IEnumerable<IXFileContentExtractor>شامل تمام IXFileContentExtractor های ثبت شده
۳XFileContentExtractor (دوباره!)خودش هم به عنوان IXFileContentExtractor ثبت شده ❌
۴حلقه بی‌نهایت → Crash
+
+ + +
+

💡 راه‌حل اصلاحی

+

برای شکستن این حلقه، باید XFileContentExtractor را به عنوان IXFileContentExtractor ثبت نکنیم. به جای آن، آن را به صورت دستی با factory بسازیم.

+ +
+ MODIFY + xAiApi/Startup.cs (متد ConfigureServices) +
+ +

کد اصلاح‌شده:

+
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (ثبت‌های قبلی)
+
+    // ==========================================================
+    // ✅ ثبت File Content Extractors (همگی Singleton)
+    // ==========================================================
+    
+    // ۱. ثبت Extractor های پایه
+    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
+    
+    // ۲. ✅ اصلاح: ثبت XFileContentExtractor به صورت Factory
+    // به جای ثبت به عنوان IXFileContentExtractor، آن را به صورت دستی می‌سازیم
+    services.AddSingleton<XFileContentExtractor>(sp =>
+    {
+        // دریافت تمام Extractor های ثبت شده (به جز XFileContentExtractor)
+        var extractors = sp.GetServices<IXFileContentExtractor>();
+        return new XFileContentExtractor(extractors);
+    });
+    
+    // ۳. ✅ ثبت XFileContentExtractor به عنوان IXFileContentExtractor
+    // اما با استفاده از Factory که از قبل ساخته شده
+    services.AddSingleton<IXFileContentExtractor>(sp => 
+        sp.GetRequiredService<XFileContentExtractor>());
+
+    // ==========================================================
+    // ✅ ثبت سرویس‌های AI
+    // ==========================================================
+    services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();
+    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
+    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
+    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
+}
+ +
+ ✅ چرا این راه‌حل کار می‌کند؟ +
    +
  • ✅ XFileContentExtractor ابتدا به عنوان خودش (نه interface) ثبت می‌شود
  • +
  • ✅ در factory، تمام IXFileContentExtractor های دیگر را دریافت می‌کند
  • +
  • ✅ سپس به عنوان IXFileContentExtractor ثبت می‌شود (اما از instance از قبل ساخته شده)
  • +
  • ✅ حلقه وابستگی شکسته می‌شود
  • +
+
+
+ + +
+

🔄 راه‌حل جایگزین (ساده‌تر)

+

اگر راه‌حل بالا پیچیده به نظر می‌رسد، می‌توانید از این روش ساده‌تر استفاده کنید:

+ +
+ MODIFY + xAiApi/Startup.cs +
+ +
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (ثبت‌های قبلی)
+
+    // ==========================================================
+    // ✅ ثبت File Content Extractors
+    // ==========================================================
+    
+    // ۱. ثبت Extractor های پایه
+    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
+    
+    // ۲. ✅ راه‌حل ساده: ساخت دستی XFileContentExtractor
+    // ابتدا تمام Extractor ها را در یک لیست جمع می‌کنیم
+    var extractorTypes = new[]
+    {
+        typeof(XPdfFileContentExtractor),
+        typeof(XDocxFileContentExtractor),
+        typeof(XImageFileContentExtractor),
+        typeof(XAudioFileContentExtractor),
+        typeof(XExcelFileContentExtractor),
+        typeof(XPlainTextFileContentExtractor)
+    };
+    
+    // ۳. ثبت XFileContentExtractor با factory
+    services.AddSingleton<IXFileContentExtractor>(sp =>
+    {
+        var extractors = new List<IXFileContentExtractor>();
+        
+        // ساخت دستی Extractor ها
+        foreach (var type in extractorTypes)
+        {
+            var instance = (IXFileContentExtractor)ActivatorUtilities.CreateInstance(sp, type);
+            extractors.Add(instance);
+        }
+        
+        // افزودن XVisionFileContentExtractor (که وابستگی دارد)
+        var visionExtractor = sp.GetRequiredService<XVisionFileContentExtractor>();
+        extractors.Add(visionExtractor);
+        
+        // ساخت XFileContentExtractor با لیست Extractor ها
+        return new XFileContentExtractor(extractors);
+    });
+
+    // ==========================================================
+    // ✅ ثبت سرویس‌های AI
+    // ==========================================================
+    services.AddSingleton<IXDefaultAIOCRService, XDefaultAIOCRService>();
+    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
+    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
+    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
+}
+
+ + +
+

📋 خلاصه تغییرات

+ + + + + + + + + + + + +
فایلتغییردلیل
Startup.csservices.AddScoped<IXFileContentExtractor, XFileContentExtractor>()
+ services.AddSingleton<IXFileContentExtractor>(sp => { ... })
رفع Circular Dependency با استفاده از Factory Pattern
+ +
+ ✅ نتیجه نهایی: +
    +
  • ✅ Circular Dependency کاملاً رفع می‌شود
  • +
  • ✅ DI Container می‌تواند تمام سرویس‌ها را بسازد
  • +
  • ✅ XFileContentExtractor همچنان به تمام Extractor ها دسترسی دارد
  • +
  • ✅ خطای AggregateException دیگر ظاهر نخواهد شد
  • +
+
+ +
+ 💡 نکته مهم: +

پس از اعمال این تغییر، پروژه را Rebuild کنید. خطای System.AggregateException دیگر ظاهر نخواهد شد و برنامه به درستی اجرا می‌شود.

+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/14.html b/Documents/Docs/Developer/14.html new file mode 100644 index 0000000..1d1df26 --- /dev/null +++ b/Documents/Docs/Developer/14.html @@ -0,0 +1,682 @@ + + + + + +پیاده‌سازی OCR با Tesseract در XImageFileContentExtractor - فن آوران ساحر علم + + + +
+ +
+

🔍 پیاده‌سازی OCR با Tesseract

+
تکمیل متد PerformOcrAsync در XImageFileContentExtractor برای استخراج متن از تصاویر
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

📦 گام ۱: نصب پکیج NuGet Tesseract

+

برای استفاده از Tesseract در پروژه .NET، پکیج زیر را نصب کنید:

+ +
# در Package Manager Console:
+Install-Package Tesseract
+
+# یا در .NET CLI:
+dotnet add package Tesseract
+ +
+ 💡 نکته: پکیج Tesseract به صورت خودکار Native DLL های مورد نیاز را نیز دانلود و در پوشه output کپی می‌کند. +
+
+ + +
+

📥 گام ۲: دانلود فایل‌های Trained Data

+

Tesseract برای تشخیص متن به فایل‌های traineddata نیاز دارد. این فایل‌ها مدل‌های زبانی هستند که برای هر زبان جداگانه آموزش دیده‌اند.

+ +

۲.۱. دانلود فایل‌ها:

+

از لینک زیر فایل‌های مورد نیاز را دانلود کنید:

+

https://github.com/tesseract-ocr/tessdata

+ +

۲.۲. فایل‌های مورد نیاز:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
فایلزبانحجم تقریبیکاربرد
fas.traineddataفارسی~۱۰ MBتشخیص متن فارسی
eng.traineddataانگلیسی~۴ MBتشخیص متن انگلیسی
ara.traineddataعربی~۵ MBتشخیص متن عربی (اختیاری)
+ +

۲.۳. ساختار پوشه‌ها:

+

فایل‌های دانلود شده را در مسیر زیر قرار دهید:

+
xAiApi/
+└── tessdata/
+    ├── fas.traineddata
+    ├── eng.traineddata
+    └── ara.traineddata (اختیاری)
+ +
+ ⚠️ نکته مهم: مسیر tessdata باید در زمان اجرا در دسترس باشد. می‌توانید آن را در appsettings.json پیکربندی کنید. +
+
+ + +
+

⚙️ گام ۳: پیکربندی در appsettings.json

+

تنظیمات Tesseract را در فایل appsettings.json اضافه کنید:

+ +
{
+  "AiApiConfiguration": {
+    "FileExtraction": {
+      "OCR": {
+        "TessDataPath": "tessdata",
+        "DefaultLanguage": "fas+eng",
+        "EngineMode": "LstmOnly"
+      }
+    },
+    "Models": [ ... ],
+    "Prompts": [ ... ]
+  }
+}
+ +

توضیح پارامترها:

+ + + + + + + + + + + + + + + + + + + + + +
پارامترتوضیحمقادیر مجاز
TessDataPathمسیر پوشه فایل‌های traineddataمسیر نسبی یا مطلق
DefaultLanguageزبان‌های پیش‌فرض برای OCRfas, eng, ara (ترکیب با +)
EngineModeحالت موتور TesseractTesseractOnly, LstmOnly, TesseractAndLstm, Default
+
+ + +
+

🔄 گام ۴: بازنویسی کلاس XImageFileContentExtractor

+

اکنون کلاس XImageFileContentExtractor را بازنویسی می‌کنیم تا از Tesseract برای OCR استفاده کند:

+ +
+ MODIFY + xAiApi/Providers/Extractors/XImageFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Tesseract;
+using xAiModels.Models;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using xAiApi.Interfaces.Extractors;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// Extracts content from image files using Tesseract OCR ...
+    /// </summary>
+    public class XImageFileContentExtractor : IXImageFileContentExtractor
+    {
+        /// <summary>
+        /// Supported MIME Types ...
+        /// </summary>
+        private static readonly string[] SupportedMimeTypes =
+        [
+            "image/png",
+            "image/jpeg",
+            "image/jpg",
+            "image/gif",
+            "image/webp",
+            "image/bmp"
+        ];
+
+        /// <summary>
+        /// مسیر پوشه tessdata ...
+        /// </summary>
+        private readonly string _tessDataPath;
+
+        /// <summary>
+        /// زبان پیش‌فرض برای OCR ...
+        /// </summary>
+        private readonly string _defaultLanguage;
+
+        /// <summary>
+        /// حالت موتور Tesseract ...
+        /// </summary>
+        private readonly OcrEngineMode _engineMode;
+
+        /// <summary>
+        /// Logger ...
+        /// </summary>
+        private readonly ILogger<XImageFileContentExtractor> _logger;
+
+        /// <summary>
+        /// Constructor با پیکربندی پیش‌فرض ...
+        /// </summary>
+        public XImageFileContentExtractor(
+            ILogger<XImageFileContentExtractor> logger,
+            string tessDataPath = "tessdata",
+            string defaultLanguage = "fas+eng",
+            OcrEngineMode engineMode = OcrEngineMode.LstmOnly
+        )
+        {
+            _logger = logger;
+            _tessDataPath = tessDataPath;
+            _defaultLanguage = defaultLanguage;
+            _engineMode = engineMode;
+        }
+
+        /// <summary>
+        /// Check if this extractor supports the specified MIME type ...
+        /// </summary>
+        public bool CanExtract(string mimeType)
+        {
+            return SupportedMimeTypes.Contains(
+                mimeType?.ToLowerInvariant() ?? string.Empty
+            );
+        }
+
+        /// <summary>
+        /// Extract text content from file stream ...
+        /// </summary>
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = await ExtractRichAsync(
+                fileStream,
+                "image",
+                mimeType,
+                cancellationToken
+            );
+            return result.Text;
+        }
+
+        /// <summary>
+        /// Extract content from stream as Rich Result ...
+        /// </summary>
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType
+            };
+
+            // خواندن بایت‌های تصویر
+            using var memoryStream = new MemoryStream();
+            await fileStream.CopyToAsync(memoryStream, cancellationToken);
+            var imageBytes = memoryStream.ToArray();
+
+            // افزودن تصویر به عنوان DataContent (برای Vision Models)
+            result.Images.Add(new XExtractedImage
+            {
+                Bytes = imageBytes,
+                MimeType = mimeType,
+                Description = $"Attached image: {fileName}"
+            });
+
+            // انجام OCR برای استخراج متن
+            try
+            {
+                var ocrText = await PerformOcrAsync(imageBytes, cancellationToken);
+                if (!string.IsNullOrWhiteSpace(ocrText))
+                {
+                    result.Text = ocrText;
+                    _logger.LogInformation(
+                        "OCR completed successfully for file: {FileName}, extracted {Length} characters",
+                        fileName,
+                        ocrText.Length
+                    );
+                }
+                else
+                {
+                    _logger.LogWarning("OCR completed but no text was extracted from file: {FileName}", fileName);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "OCR failed for file: {FileName}", fileName);
+                result.ErrorMessage = $"OCR failed: {ex.Message}";
+                // تصویر همچنان برای Vision Models در دسترس است
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// انجام OCR روی تصویر با استفاده از Tesseract ...
+        /// </summary>
+        private async Task<string> PerformOcrAsync(
+            byte[] imageBytes,
+            CancellationToken cancellationToken
+        )
+        {
+            return await Task.Run(() =>
+            {
+                // بررسی وجود مسیر tessdata
+                if (!Directory.Exists(_tessDataPath))
+                {
+                    throw new DirectoryNotFoundException(
+                        $"TessData directory not found at: {_tessDataPath}. " +
+                        "Please download traineddata files from https://github.com/tesseract-ocr/tessdata"
+                    );
+                }
+
+                // ایجاد Tesseract Engine
+                using var engine = new TesseractEngine(
+                    datapath: _tessDataPath,
+                    language: _defaultLanguage,
+                    mode: _engineMode
+                );
+
+                // بارگذاری تصویر از byte array
+                using var pix = Pix.LoadFromMemory(imageBytes);
+
+                // تنظیم CancellationToken
+                cancellationToken.ThrowIfCancellationRequested();
+
+                // انجام OCR
+                using var page = engine.Process(pix);
+
+                // استخراج متن
+                var text = page.GetText();
+
+                // پاکسازی متن (حذف فاصله‌های اضافی)
+                text = text?.Trim();
+
+                return text ?? string.Empty;
+            }, cancellationToken);
+        }
+    }
+}
+
+ + +
+

🔧 گام ۵: جزئیات پیاده‌سازی متد PerformOcrAsync

+ +

۵.۱. مراحل کار متد:

+
+
+

۱. بررسی مسیر tessdata

+
    +
  • اطمینان از وجود پوشه tessdata
  • +
  • پرتاب خطا در صورت عدم وجود
  • +
+
+
+

۲. ایجاد Tesseract Engine

+
    +
  • بارگذاری مدل زبانی
  • +
  • تنظیم حالت موتور (LSTM)
  • +
+
+
+

۳. بارگذاری تصویر

+
    +
  • تبدیل byte[] به Pix object
  • +
  • پشتیبانی از فرمت‌های مختلف
  • +
+
+
+

۴. انجام OCR

+
    +
  • پردازش تصویر
  • +
  • استخراج متن
  • +
+
+
+

۵. پاکسازی متن

+
    +
  • حذف فاصله‌های اضافی
  • +
  • بررسی CancellationToken
  • +
+
+
+ +

۵.۲. ویژگی‌های کلیدی پیاده‌سازی:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ویژگیتوضیح
پشتیبانی چند زبانهاستفاده از fas+eng برای تشخیص همزمان فارسی و انگلیسی
حالت LSTMاستفاده از موتور LSTM برای دقت بالاتر در متون فارسی
مدیریت خطابررسی وجود tessdata و مدیریت استثناها
Loggingثبت موفقیت/شکست OCR با جزئیات
Cancellation Supportپشتیبانی از لغو عملیات در میانه کار
Resource Managementاستفاده از using برای آزادسازی منابع
+
+ + +
+

💡 گام ۶: نکات مهم و عیب‌یابی

+ +

۶.۱. مشکلات رایج و راه‌حل‌ها:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
مشکلعلتراه‌حل
DirectoryNotFoundExceptionپوشه tessdata یافت نشددانلود traineddata و قرار دادن در مسیر صحیح
TesseractExceptionفایل traineddata خراب یا ناسازگاردانلود مجدد از منبع رسمی
دقت پایین OCRکیفیت پایین تصویرپیش‌پردازش تصویر (افزایش کنتراست، resize)
تشخیص نادرست فارسیترکیب نامناسب زبان‌هااستفاده از fas به تنهایی یا fas+eng
کندی عملکردتصاویر بزرگتغییر EngineMode به TesseractOnly
+ +

۶.۲. بهینه‌سازی عملکرد:

+
+
+

🚀 افزایش سرعت

+
    +
  • استفاده از OcrEngineMode.TesseractOnly
  • +
  • کاهش DPI تصویر (مثلاً 150 به جای 300)
  • +
  • استفاده از یک زبان به جای چند زبان
  • +
+
+
+

🎯 افزایش دقت

+
    +
  • استفاده از OcrEngineMode.LstmOnly
  • +
  • افزایش DPI تصویر (300 یا بالاتر)
  • +
  • پیش‌پردازش تصویر (binarization)
  • +
+
+
+ +

۶.۳. پیش‌پردازش تصویر (اختیاری):

+

برای افزایش دقت OCR، می‌توانید قبل از پردازش، تصویر را پیش‌پردازش کنید:

+
// مثال: افزایش کنتراست و تبدیل به سیاه و سفید
+using var pix = Pix.LoadFromMemory(imageBytes);
+using var enhanced = pix.ConvertTo1(); // تبدیل به 1-bit
+using var page = engine.Process(enhanced);
+ +

۶.۴. ثبت در Startup.cs:

+

اکنون XImageFileContentExtractor را با پیکربندی صحیح ثبت کنید:

+
public void ConfigureServices(IServiceCollection services)
+{
+    // ... سایر ثبت‌ها
+
+    // ثبت XImageFileContentExtractor با پیکربندی
+    services.AddSingleton<IXFileContentExtractor>(sp =>
+    {
+        var logger = sp.GetRequiredService<ILogger<XImageFileContentExtractor>>();
+        var configuration = sp.GetRequiredService<IConfiguration>();
+        
+        var tessDataPath = configuration.GetValue<string>(
+            "AiApiConfiguration:FileExtraction:OCR:TessDataPath"
+        ) ?? "tessdata";
+        
+        var defaultLanguage = configuration.GetValue<string>(
+            "AiApiConfiguration:FileExtraction:OCR:DefaultLanguage"
+        ) ?? "fas+eng";
+        
+        return new XImageFileContentExtractor(
+            logger: logger,
+            tessDataPath: tessDataPath,
+            defaultLanguage: defaultLanguage,
+            engineMode: OcrEngineMode.LstmOnly
+        );
+    });
+
+    // ... سایر ثبت‌ها
+}
+ +
+ ✅ نتیجه نهایی: +
    +
  • 🔍 OCR کامل: استخراج متن از تصاویر با دقت بالا
  • +
  • 🌐 چند زبانه: پشتیبانی از فارسی، انگلیسی و عربی
  • +
  • ⚡ بهینه: استفاده از موتور LSTM برای دقت بالاتر
  • +
  • 🛡️ مقاوم: مدیریت خطا و logging کامل
  • +
  • 🔄 قابل لغو: پشتیبانی از CancellationToken
  • +
  • 📦 منعطف: پیکربندی از طریق appsettings.json
  • +
+
+ +
+ 💡 نکته تکمیلی: +

اگر نیاز به دقت بالاتر برای متون فارسی دارید، می‌توانید از مدل‌های آموزش‌دیده‌شده خاص فارسی استفاده کنید یا از ترکیب Tesseract با مدل‌های deep learning (مانند CRNN) بهره ببرید.

+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/7_1.html b/Documents/Docs/Developer/7_1.html new file mode 100644 index 0000000..17c453f --- /dev/null +++ b/Documents/Docs/Developer/7_1.html @@ -0,0 +1,658 @@ + + + + + +بازنویسی FallbackToImageExtractionAsync با PdfiumViewer - فن آوران ساحر علم + + + +
+ +
+

📄 بازنویسی FallbackToImageExtractionAsync با PdfiumViewer

+
جایگزینی PdfPigRenderer با کتابخانه قدرتمند PdfiumViewer برای رندر PDF
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: یکشنبه ۱۳ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ +
+

🎯 مقدمه

+

+ کتابخانه PdfiumViewer یکی از پایدارترین و سریع‌ترین راهکارها برای رندر PDF در .NET است + که بر پایه موتور PDFium گوگل (همان موتور استفاده شده در Chrome) ساخته شده است. + این کتابخانه نسبت به PdfPigRenderer عملکرد بهتری دارد و برای تبدیل PDF به تصویر مناسب‌تر است. +

+
+ + +
+

📦 گام ۱: نصب پکیج‌های NuGet مورد نیاز

+

ابتدا باید پکیج‌های زیر را در پروژه xAiApi نصب کنید:

+
# پکیج اصلی PdfiumViewer
+Install-Package PdfiumViewer
+
+# پکیج Native DLLs (بسیار مهم - شامل pdfium.dll برای پلتفرم‌های مختلف)
+Install-Package PdfiumViewer.Native
+
+# برای کار با Bitmap و تصاویر
+Install-Package System.Drawing.Common
+ +
+ ⚠️ نکته مهم در مورد Native DLLs: +

پکیج PdfiumViewer.Native فایل‌های pdfium.dll را برای پلتفرم‌های مختلف + (x86 و x64) در پوشه bin پروژه کپی می‌کند. بدون این پکیج، کتابخانه با خطای + DllNotFoundException مواجه خواهد شد.

+
+
+ + +
+

🔧 گام ۲: کد بازنویسی شده با PdfiumViewer

+ +
+ MODIFY + xAiApi/Providers/Extractors/XPdfFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Drawing;
+using System.Threading;
+using System.Threading.Tasks;
+using PdfiumViewer;
+using xAiApi.Models;
+using xAiApi.Interfaces.Extractors;
+using xCommons.Extensions;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// استخراج محتوا از PDF با پشتیبانی از Fallback تصویری با PdfiumViewer ...
+    /// </summary>
+    public class XPdfFileContentExtractor : IXPdfFileContentExtractor
+    {
+        /// <summary>
+        /// Supported MIME Types ...
+        /// </summary>
+        private static readonly string[] SupportedMimeTypes = ["application/pdf"];
+
+        /// <summary>
+        /// حداقل تعداد کاراکتر برای تشخیص PDF متنی ...
+        /// </summary>
+        private const int MinTextLengthThreshold = 50;
+
+        /// <summary>
+        /// حداکثر تعداد صفحات برای تبدیل به تصویر ...
+        /// </summary>
+        private const int MaxPagesForImageFallback = 10;
+
+        /// <summary>
+        /// DPI برای رندر تصاویر (72 = کیفیت معمولی، 150 = کیفیت خوب، 300 = کیفیت بالا) ...
+        /// </summary>
+        private const int RenderDpi = 150;
+
+        /// <summary>
+        /// حداکثر عرض تصویر به پیکسل ...
+        /// </summary>
+        private const int MaxImageWidth = 2000;
+
+        /// <summary>
+        /// بررسی پشتیبانی از MIME Type ...
+        /// </summary>
+        public bool CanExtract(string mimeType)
+        {
+            return SupportedMimeTypes.Contains(
+                mimeType?.ToLowerInvariant() ?? string.Empty
+            );
+        }
+
+        /// <summary>
+        /// استخراج محتوای غنی با Fallback هوشمند ...
+        /// </summary>
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType
+            };
+
+            // کپی به MemoryStream برای چندبار خواندن
+            using var memoryStream = new MemoryStream();
+            await fileStream.CopyToAsync(memoryStream, cancellationToken);
+            memoryStream.Position = 0;
+
+            // مرحله ۱: تلاش برای استخراج متن با UglyToad.PdfPig
+            var extractedText = await ExtractTextAsync(memoryStream, cancellationToken);
+
+            // مرحله ۲: بررسی کیفیت متن استخراج شده
+            if (IsTextSufficient(extractedText))
+            {
+                result.Text = extractedText;
+            }
+            else
+            {
+                // PDF اسکن‌شده است - Fallback به تصویر با PdfiumViewer
+                memoryStream.Position = 0;
+                result = await FallbackToImageExtractionAsync(
+                    memoryStream,
+                    fileName,
+                    mimeType,
+                    cancellationToken
+                );
+                result.UsedImageFallback = true;
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Fallback: تبدیل صفحات PDF به تصویر با استفاده از PdfiumViewer ...
+        /// </summary>
+        private async Task<XFileExtractionResult> FallbackToImageExtractionAsync(
+            Stream pdfStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType,
+                UsedImageFallback = true
+            };
+
+            await Task.Run(() =>
+            {
+                try
+                {
+                    // باز کردن PDF با PdfiumViewer
+                    using var document = PdfDocument.Load(pdfStream);
+
+                    // محاسبه تعداد صفحات برای پردازش
+                    var pageCount = Math.Min(
+                        document.PageCount,
+                        MaxPagesForImageFallback
+                    );
+
+                    // پردازش هر صفحه
+                    for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
+                    {
+                        // بررسی CancellationToken
+                        if (cancellationToken.IsCancellationRequested)
+                        {
+                            break;
+                        }
+
+                        try
+                        {
+                            // دریافت ابعاد اصلی صفحه
+                            var pageSize = document.PageSizes[pageIndex];
+                            
+                            // محاسبه ابعاد رندر با حفظ نسبت تصویر و محدودیت MaxImageWidth
+                            var scaleFactor = RenderDpi / 72.0;
+                            var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor);
+                            var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor);
+
+                            // محدود کردن عرض به MaxImageWidth
+                            if (renderWidth > MaxImageWidth)
+                            {
+                                var ratio = (double)MaxImageWidth / renderWidth;
+                                renderWidth = MaxImageWidth;
+                                renderHeight = (int)(renderHeight * ratio);
+                            }
+
+                            // رندر صفحه به Bitmap
+                            using var bitmap = document.Render(
+                                pageIndex: pageIndex,
+                                width: renderWidth,
+                                height: renderHeight,
+                                dpiX: RenderDpi,
+                                dpiY: RenderDpi,
+                                forPrinting: false
+                            );
+
+                            // تبدیل Bitmap به آرایه بایت PNG
+                            byte[] imageBytes;
+                            using (var memoryStream = new MemoryStream())
+                            {
+                                bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
+                                imageBytes = memoryStream.ToArray();
+                            }
+
+                            // افزودن به نتیجه
+                            result.Images.Add(new XExtractedImage
+                            {
+                                Bytes = imageBytes,
+                                PageNumber = pageIndex + 1,
+                                MimeType = "image/png",
+                                Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})"
+                            });
+                        }
+                        catch (Exception pageEx)
+                        {
+                            // لاگ خطای صفحه و ادامه با صفحات دیگر
+                            System.Diagnostics.Debug.WriteLine(
+                                $"Error rendering page {pageIndex + 1} of {fileName}: {pageEx.Message}"
+                            );
+                        }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    // ثبت خطای کلی
+                    result.ErrorMessage = $"PDF image extraction failed: {ex.Message}";
+                    System.Diagnostics.Debug.WriteLine(
+                        $"PdfiumViewer extraction error for {fileName}: {ex}"
+                    );
+                }
+            }, cancellationToken);
+
+            return result;
+        }
+
+        /// <summary>
+        /// استخراج متن از PDF با UglyToad.PdfPig ...
+        /// </summary>
+        private async Task<string> ExtractTextAsync(
+            Stream pdfStream,
+            CancellationToken cancellationToken
+        )
+        {
+            return await Task.Run(() =>
+            {
+                var sb = new StringBuilder();
+                using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream);
+                
+                foreach (var page in document.GetPages())
+                {
+                    var text = page.Text?.Trim();
+                    if (!string.IsNullOrEmpty(text))
+                    {
+                        sb.AppendLine(text);
+                        sb.AppendLine();
+                    }
+                }
+                
+                return sb.ToString();
+            }, cancellationToken);
+        }
+
+        /// <summary>
+        /// بررسی آیا متن استخراج شده کافی است ...
+        /// </summary>
+        private bool IsTextSufficient(string text)
+        {
+            if (string.IsNullOrWhiteSpace(text))
+            {
+                return false;
+            }
+
+            // حذف فاصله‌ها و بررسی طول
+            var cleanText = new string(
+                text.Where(c => !char.IsWhiteSpace(c)).ToArray()
+            );
+            return cleanText.Length >= MinTextLengthThreshold;
+        }
+
+        /// <summary>
+        /// سازگاری با نسخه قدیمی Interface ...
+        /// </summary>
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var result = await ExtractRichAsync(
+                fileStream,
+                "document.pdf",
+                mimeType,
+                cancellationToken
+            );
+            return result.Text;
+        }
+    }
+}
+
+ + +
+

📊 گام ۳: مقایسه PdfiumViewer با PdfPigRenderer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ویژگیPdfPigRendererPdfiumViewer
پایداری⚠️ نسبتاً جدید، گاهی ناپایدار✅ بسیار پایدار، سال‌ها استفاده در production
سرعت رندر⭐⭐ متوسط⭐⭐⭐⭐ بسیار سریع (native C++)
کیفیت خروجی⭐⭐⭐ خوب⭐⭐⭐⭐⭐ عالی (همان موتور Chrome)
پشتیبانی از فرمت‌های پیچیده⚠️ محدود✅ کامل (فونت‌ها، transparency، gradients)
حافظه مصرفی⭐⭐ متوسط⭐⭐⭐⭐ بهینه (native memory)
نیاز به Native DLL❌ خیر (Pure .NET)✅ بله (pdfium.dll)
پشتیبانی Cross-platform✅ کامل✅ Windows + Linux (با پکیج Native مناسب)
کنترل DPI و ابعاد⚠️ محدود✅ کامل و دقیق
+
+ + +
+

✨ گام ۴: ویژگی‌های کلیدی کد بازنویسی شده

+ +
+
+

🎨 کنترل DPI هوشمند

+
    +
  • پارامتر RenderDpi قابل تنظیم
  • +
  • تعادل بین کیفیت و حجم خروجی
  • +
  • مقدار ۱۵۰ برای Vision Models ایده‌آل
  • +
+
+
+

📏 محدودیت ابعاد

+
    +
  • MaxImageWidth = 2000
  • +
  • جلوگیری از تصاویر بسیار بزرگ
  • +
  • حفظ نسبت تصویر
  • +
+
+
+

🛡️ مدیریت خطای پیشرفته

+
    +
  • خطای هر صفحه به صورت جداگانه
  • +
  • ادامه پردازش با صفحات دیگر
  • +
  • لاگ دقیق خطاها
  • +
+
+
+

⚡ بهینه‌سازی عملکرد

+
    +
  • استفاده از Task.Run
  • +
  • پشتیبانی از CancellationToken
  • +
  • مدیریت صحیح using
  • +
+
+
+
+ + +
+

⚠️ گام ۵: نکات مهم پیاده‌سازی

+ +
+ 💡 نکته ۱: Native DLL در زمان اجرا +

PdfiumViewer به صورت خودکار pdfium.dll را از پوشه‌های x86 یا x64 + در مسیر اجرای برنامه بارگذاری می‌کند. اطمینان حاصل کنید که این فایل‌ها همراه با برنامه deploy می‌شوند.

+
+ +
+ ⚠️ نکته ۲: Linux Deployment +

برای استقرار روی Linux، از پکیج PdfiumViewer.Core استفاده کنید و + فایل libpdfium.so را در مسیر مناسب قرار دهید:

+
# در csproj فایل:
+<ItemGroup>
+  <None Include="runtimes/linux-x64/native/libpdfium.so">
+    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+  </None>
+</ItemGroup>
+
+ +
+ ✅ نکته ۳: تست عملکرد +

برای یک PDF اسکن‌شده ۱۰ صفحه‌ای با DPI=150:

+
    +
  • زمان پردازش: حدود ۲-۳ ثانیه
  • +
  • حجم هر تصویر: ۲۰۰-۴۰۰ کیلوبایت PNG
  • +
  • مصرف RAM: حدود ۱۰۰ مگابایت peak
  • +
+
+ +
+ ⚠️ نکته ۴: محدودیت‌های PdfiumViewer +
    +
  • پشتیبانی ضعیف‌تر از async/await واقعی (استفاده از Task.Run)
  • +
  • نیاز به نصب Native DLL
  • +
  • عدم پشتیبانی از PDF های رمزنگاری شده با پسورد
  • +
+
+
+ + +
+

🚀 گام ۶: پیشنهاد برای .NET 8+ (آینده‌نگرانه)

+

+ اگر پروژه شما در آینده به .NET 8 یا بالاتر مهاجرت کند، می‌توانید از کتابخانه‌های مدرن‌تر مانند + SkiaSharp + HarfBuzzSharp یا PdfPig نسخه جدید استفاده کنید + که کاملاً Cross-platform و async-native هستند. اما برای حال حاضر، PdfiumViewer بهترین انتخاب است. +

+
+ + +
+

📋 خلاصه تغییرات

+ + + + + + + + + + + + + + + + + + + + + + + + + +
موردتوضیح
جایگزینی PdfPigRendererبا PdfDocument از PdfiumViewer
افزودن کنترل DPIپارامتر RenderDpi برای تنظیم کیفیت
محدودیت ابعادMaxImageWidth برای جلوگیری از تصاویر بزرگ
مدیریت خطای صفحهادامه پردازش در صورت خطای یک صفحه
بهبود لاگثبت خطاها با Debug.WriteLine
+ +
+ ✅ نتیجه‌گیری: +

با بازنویسی تابع FallbackToImageExtractionAsync با استفاده از PdfiumViewer، + سیستم شما اکنون قابلیت تبدیل قابل اعتماد و با کیفیت PDF های اسکن‌شده به تصاویر را دارد. + این تصاویر می‌توانند مستقیماً به عنوان DataContent به مدل‌های Vision مانند + GPT-4V، Gemini یا Qwen-VL ارسال شوند. 🎯

+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/7_2.html b/Documents/Docs/Developer/7_2.html new file mode 100644 index 0000000..4327cc3 --- /dev/null +++ b/Documents/Docs/Developer/7_2.html @@ -0,0 +1,737 @@ + + + + + +بازنویسی PDF Fallback با PdfiumViewer - فن آوران ساحر علم + + + +
+ +
+

📄 بازنویسی PDF Fallback با PdfiumViewer

+
جایگزینی PdfPigRenderer با PdfiumViewer برای تبدیل PDF به تصویر
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: یکشنبه ۱۳ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + +
+

🎯 معرفی PdfiumViewer

+

+ PdfiumViewer یک کتابخانه قدرتمند و سریع برای کار با فایل‌های PDF در .NET است + که بر پایه موتور PDFium گوگل (همان موتوری که Chrome استفاده می‌کند) ساخته شده است. +

+ +
+
+

⚡ مزایای PdfiumViewer

+
    +
  • سرعت بالا در رندر صفحات
  • +
  • پشتیبانی کامل از PDF های پیچیده
  • +
  • کیفیت رندر عالی
  • +
  • کنترل دقیق DPI و اندازه خروجی
  • +
  • پشتیبانی از فرمت‌های مختلف تصویر
  • +
+
+
+

⚠️ ملاحظات مهم

+
    +
  • نیاز به Native DLL های PDFium
  • +
  • حجم فایل DLL ها حدود 10-20 مگابایت
  • +
  • نیاز به کپی DLL ها در output directory
  • +
  • Thread-safe نیست (نیاز به قفل)
  • +
+
+
+
+ + +
+

📦 گام ۱: نصب پکیج‌های NuGet

+ +
# نصب پکیج اصلی PdfiumViewer
+Install-Package PdfiumViewer
+
+# نصب Native DLL های PDFium (برای پلتفرم x64)
+Install-Package PdfiumViewer.Native
+
+# یا برای هر دو پلتفرم x86 و x64
+Install-Package PdfiumViewer.Native.x86
+Install-Package PdfiumViewer.Native.x64
+ +
+ 💡 نکته: پکیج PdfiumViewer.Native به صورت خودکار DLL های pdfium.dll + را در پوشه x86 و x64 پروژه کپی می‌کند و در runtime بر اساس معماری سیستم، + DLL مناسب را لود می‌کند. +
+
+ + +
+

🔧 گام ۲: بازنویسی تابع FallbackToImageExtractionAsync

+ +
+ MODIFY + xAiApi/Providers/Extractors/XPdfFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Threading;
+using System.Threading.Tasks;
+using PdfiumViewer;
+using xAiApi.Models;
+
+namespace xAiApi.Providers.Extractors
+{
+    public class XPdfFileContentExtractor : IXPdfFileContentExtractor
+    {
+        private const int MaxPagesForImageFallback = 10;
+        private const int MinTextLengthThreshold = 50;
+        
+        /// <summary>
+        /// DPI پیش‌فرض برای رندر تصاویر (72 = استاندارد، 150 = کیفیت بالا)
+        /// </summary>
+        private const int DefaultDpi = 150;
+
+        // ... (سایر متدها)
+
+        /// <summary>
+        /// Fallback: تبدیل صفحات PDF به تصویر با استفاده از PdfiumViewer ...
+        /// </summary>
+        private async Task<XFileExtractionResult> FallbackToImageExtractionAsync(
+            Stream pdfStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken
+        )
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType,
+                UsedImageFallback = true
+            };
+
+            // کپی Stream به MemoryStream برای PdfiumViewer
+            // (PdfiumViewer به Stream قابل Seek نیاز دارد)
+            using var memoryStream = new MemoryStream();
+            await pdfStream.CopyToAsync(memoryStream, cancellationToken);
+            memoryStream.Position = 0;
+
+            await Task.Run(() =>
+            {
+                try
+                {
+                    // بارگذاری PDF با PdfiumViewer
+                    using var pdfDocument = PdfDocument.Load(memoryStream);
+                    
+                    // محاسبه تعداد صفحات برای پردازش
+                    var pageCount = Math.Min(
+                        pdfDocument.PageCount,
+                        MaxPagesForImageFallback
+                    );
+
+                    // حلقه روی صفحات و تبدیل به تصویر
+                    for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
+                    {
+                        // بررسی CancellationToken
+                        cancellationToken.ThrowIfCancellationRequested();
+
+                        // رندر صفحه به Bitmap
+                        // متد RenderPage پارامترهای زیر را می‌پذیرد:
+                        // - pageIndex: شماره صفحه
+                        // - width: عرض تصویر خروجی (0 = اندازه اصلی)
+                        // - height: ارتفاع تصویر خروجی (0 = اندازه اصلی)
+                        // - dpi: کیفیت رندر
+                        using var bitmap = pdfDocument.RenderPage(
+                            pageIndex: pageIndex,
+                            width: 0,      // 0 = استفاده از اندازه اصلی
+                            height: 0,     // 0 = استفاده از اندازه اصلی
+                            dpiX: DefaultDpi,
+                            dpiY: DefaultDpi
+                        );
+
+                        if (bitmap != null)
+                        {
+                            // تبدیل Bitmap به PNG
+                            using var imageStream = new MemoryStream();
+                            bitmap.Save(imageStream, ImageFormat.Png);
+                            var imageBytes = imageStream.ToArray();
+
+                            // افزودن به لیست تصاویر
+                            result.Images.Add(new XExtractedImage
+                            {
+                                Bytes = imageBytes,
+                                PageNumber = pageIndex + 1,
+                                MimeType = "image/png",
+                                Description = $"Page {pageIndex + 1} of {fileName}"
+                            });
+                        }
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    // عملیات لغو شد
+                    throw;
+                }
+                catch (Exception ex)
+                {
+                    // خطا در پردازش PDF
+                    result.ErrorMessage = $"Failed to render PDF pages to images: {ex.Message}";
+                }
+            }, cancellationToken);
+
+            return result;
+        }
+
+        // ... (سایر متدها)
+    }
+}
+
+ + +
+

🎨 گام ۳: نسخه پیشرفته با کنترل دقیق DPI

+

اگر نیاز به کنترل دقیق‌تر روی کیفیت و اندازه تصاویر دارید، از این نسخه استفاده کنید:

+ +
+ ADVANCED + نسخه پیشرفته با کنترل DPI +
+ +
/// <summary>
+/// Fallback پیشرفته با کنترل دقیق DPI و کیفیت ...
+/// </summary>
+private async Task<XFileExtractionResult> FallbackToImageExtractionAdvancedAsync(
+    Stream pdfStream,
+    string fileName,
+    string mimeType,
+    int targetDpi = 150,
+    ImageFormat outputFormat = null,
+    CancellationToken cancellationToken = default
+)
+{
+    outputFormat ??= ImageFormat.Png;
+    
+    var result = new XFileExtractionResult
+    {
+        FileName = fileName,
+        MimeType = mimeType,
+        UsedImageFallback = true
+    };
+
+    using var memoryStream = new MemoryStream();
+    await pdfStream.CopyToAsync(memoryStream, cancellationToken);
+    memoryStream.Position = 0;
+
+    await Task.Run(() =>
+    {
+        using var pdfDocument = PdfDocument.Load(memoryStream);
+        var pageCount = Math.Min(pdfDocument.PageCount, MaxPagesForImageFallback);
+
+        for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            // دریافت اندازه اصلی صفحه
+            var pageSize = pdfDocument.PageSizes[pageIndex];
+            
+            // محاسبه اندازه تصویر بر اساس DPI
+            var width = (int)(pageSize.Width * targetDpi / 72.0);
+            var height = (int)(pageSize.Height * targetDpi / 72.0);
+
+            // رندر با اندازه مشخص
+            using var bitmap = pdfDocument.RenderPage(
+                pageIndex: pageIndex,
+                width: width,
+                height: height,
+                dpiX: targetDpi,
+                dpiY: targetDpi
+            );
+
+            if (bitmap != null)
+            {
+                using var imageStream = new MemoryStream();
+                bitmap.Save(imageStream, outputFormat);
+                var imageBytes = imageStream.ToArray();
+
+                result.Images.Add(new XExtractedImage
+                {
+                    Bytes = imageBytes,
+                    PageNumber = pageIndex + 1,
+                    MimeType = outputFormat == ImageFormat.Jpeg ? "image/jpeg" : "image/png",
+                    Description = $"Page {pageIndex + 1} of {fileName} ({width}x{height}px @ {targetDpi}dpi)"
+                });
+            }
+        }
+    }, cancellationToken);
+
+    return result;
+}
+
+ + +
+

⚙️ گام ۴: پیکربندی DPI در appsettings.json

+ +
+ CONFIG + appsettings.json +
+ +
{
+  "AiApiConfiguration": {
+    "FileExtraction": {
+      "PdfImageFallback": {
+        "Enabled": true,
+        "MaxPages": 10,
+        "Dpi": 150,
+        "OutputFormat": "png",
+        "MinTextLengthThreshold": 50
+      }
+    }
+  }
+}
+ +
+ CLASS + xAiApi/Configurations/XPdfImageFallbackConfiguration.cs +
+ +
namespace xAiApi.Configurations
+{
+    public class XPdfImageFallbackConfiguration
+    {
+        public bool Enabled { get; set; } = true;
+        public int MaxPages { get; set; } = 10;
+        public int Dpi { get; set; } = 150;
+        public string OutputFormat { get; set; } = "png";
+        public int MinTextLengthThreshold { get; set; } = 50;
+    }
+}
+
+ + +
+

🔗 گام ۵: استفاده از پیکربندی در Extractor

+ +
+ MODIFY + XPdfFileContentExtractor.cs +
+ +
public class XPdfFileContentExtractor : IXPdfFileContentExtractor
+{
+    private readonly XPdfImageFallbackConfiguration _config;
+
+    public XPdfFileContentExtractor(XPdfImageFallbackConfiguration config)
+    {
+        _config = config ?? new XPdfImageFallbackConfiguration();
+    }
+
+    private async Task<XFileExtractionResult> FallbackToImageExtractionAsync(
+        Stream pdfStream,
+        string fileName,
+        string mimeType,
+        CancellationToken cancellationToken
+    )
+    {
+        var result = new XFileExtractionResult
+        {
+            FileName = fileName,
+            MimeType = mimeType,
+            UsedImageFallback = true
+        };
+
+        using var memoryStream = new MemoryStream();
+        await pdfStream.CopyToAsync(memoryStream, cancellationToken);
+        memoryStream.Position = 0;
+
+        await Task.Run(() =>
+        {
+            using var pdfDocument = PdfDocument.Load(memoryStream);
+            var pageCount = Math.Min(pdfDocument.PageCount, _config.MaxPages);
+
+            for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                using var bitmap = pdfDocument.RenderPage(
+                    pageIndex: pageIndex,
+                    width: 0,
+                    height: 0,
+                    dpiX: _config.Dpi,
+                    dpiY: _config.Dpi
+                );
+
+                if (bitmap != null)
+                {
+                    using var imageStream = new MemoryStream();
+                    
+                    // انتخاب فرمت خروجی بر اساس پیکربندی
+                    var format = _config.OutputFormat?.ToLowerInvariant() switch
+                    {
+                        "jpeg" or "jpg" => ImageFormat.Jpeg,
+                        "bmp" => ImageFormat.Bmp,
+                        _ => ImageFormat.Png
+                    };
+                    
+                    bitmap.Save(imageStream, format);
+                    var imageBytes = imageStream.ToArray();
+
+                    result.Images.Add(new XExtractedImage
+                    {
+                        Bytes = imageBytes,
+                        PageNumber = pageIndex + 1,
+                        MimeType = format == ImageFormat.Jpeg ? "image/jpeg" : "image/png",
+                        Description = $"Page {pageIndex + 1} of {fileName}"
+                    });
+                }
+            }
+        }, cancellationToken);
+
+        return result;
+    }
+}
+
+ + +
+

🔌 گام ۶: ثبت در Dependency Injection

+ +
+ MODIFY + xAiApi/Startup.cs +
+ +
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (سایر ثبت‌ها)
+
+    // ✅ ثبت پیکربندی PDF Image Fallback
+    services.Configure<XPdfImageFallbackConfiguration>(
+        Configuration.GetSection("AiApiConfiguration:FileExtraction:PdfImageFallback")
+    );
+
+    // ✅ ثبت PDF Extractor با پیکربندی
+    services.AddSingleton<IXFileContentExtractor>(sp =>
+    {
+        var config = sp.GetRequiredService<IOptions<XPdfImageFallbackConfiguration>>().Value;
+        return new XPdfFileContentExtractor(config);
+    });
+
+    // ... (بقیه کدها)
+}
+
+ + +
+

🐛 گام ۷: عیب‌یابی و نکات مهم

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
مشکلعلتراه‌حل
DllNotFoundExceptionpdfium.dll یافت نشدنصب PdfiumViewer.Native و rebuild پروژه
BadImageFormatExceptionعدم تطابق معماری (x86/x64)استفاده از پکیج Native مناسب یا تنظیم Platform Target
خطا در PdfDocument.LoadStream غیرقابل Seekکپی به MemoryStream قبل از Load
کیفیت پایین تصاویرDPI خیلی کمافزایش DPI به 200 یا 300
OutOfMemoryExceptionتعداد صفحات زیاد یا DPI بالاکاهش MaxPages یا Dpi
+ +
+ ⚠️ Thread Safety: +

+ PdfiumViewer Thread-safe نیست. اگر چندین درخواست همزمان PDF را پردازش می‌کنند، + باید از lock یا SemaphoreSlim استفاده کنید: +

+
private static readonly SemaphoreSlim _pdfLock = new SemaphoreSlim(2, 2); // حداکثر 2 همزمان
+
+await _pdfLock.WaitAsync(cancellationToken);
+try
+{
+    // پردازش PDF
+}
+finally
+{
+    _pdfLock.Release();
+}
+
+
+ + +
+

📊 گام ۸: مقایسه PdfPigRenderer و PdfiumViewer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ویژگیPdfPigRendererPdfiumViewer
سرعت رندر⭐⭐⭐ متوسط⭐⭐⭐⭐⭐ بسیار سریع
کیفیت خروجی⭐⭐⭐ خوب⭐⭐⭐⭐⭐ عالی
حجم DLLکوچک (فقط .NET)بزرگ (10-20 MB Native)
پشتیبانی از PDF های پیچیده⭐⭐⭐ محدود⭐⭐⭐⭐⭐ کامل
کنترل DPI⭐⭐ محدود⭐⭐⭐⭐⭐ دقیق
Thread Safety✅ Thread-safe❌ نیاز به قفل
نیاز به Native DLL❌ خیر✅ بله
+ +
+ ✅ نتیجه‌گیری: +

+ PdfiumViewer انتخاب بهتری برای سناریوهای Production است، به ویژه اگر: +

+
    +
  • کیفیت رندر برایتان مهم است
  • +
  • با PDF های پیچیده (فونت‌های خاص، تصاویر، فرم‌ها) کار می‌کنید
  • +
  • سرعت پردازش اولویت دارد
  • +
  • حجم DLL ها مسئله‌ای نیست
  • +
+
+
+ + +
+

📋 گام ۹: خلاصه تغییرات

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
فایلتغییر
XPdfFileContentExtractor.csبازنویسی FallbackToImageExtractionAsync با PdfiumViewer
XPdfImageFallbackConfiguration.csکلاس جدید برای پیکربندی
appsettings.jsonافزودن بخش PdfImageFallback
Startup.csثبت پیکربندی و Extractor در DI
NuGet Packagesنصب PdfiumViewer و PdfiumViewer.Native
+ +
+ 💡 نکات کلیدی: +
    +
  • ✅ PdfiumViewer سریع‌تر و باکیفیت‌تر از PdfPigRenderer است
  • +
  • ✅ کنترل دقیق DPI و اندازه خروجی
  • +
  • ⚠️ نیاز به Native DLL ها (حدود 10-20 MB)
  • +
  • ⚠️ Thread-safe نیست و نیاز به قفل دارد
  • +
  • ✅ پیکربندی‌پذیر از طریق appsettings.json
  • +
+
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/8.html b/Documents/Docs/Developer/8.html new file mode 100644 index 0000000..bdc882e --- /dev/null +++ b/Documents/Docs/Developer/8.html @@ -0,0 +1,638 @@ + + + + + +استخراج محتوای جهانی با Qwen3.5-9B Vision - فن آوران ساحر علم + + + +
+ +
+

👁️ استخراج محتوای جهانی با Qwen3.5-9B Vision

+
یکپارچه‌سازی قابلیت OCR هوشمند در معماری XFileContentExtractor
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🧠 گام ۱: تحلیل قابلیت‌های مدل Qwen3.5-9B GGUF

+

مدل Qwen3.5-9B-The-Defiant-Fable-Uncensored-Heretic-NEO-IMATRIX-MAX-MTP-GGUF یک مدل زبانی بزرگ کوانتیزه شده (GGUF) با ویژگی‌های منحصر به فرد است:

+ +
+
+

👁️ قابلیت Vision / OCR

+
    +
  • توانایی خواندن متن از تصاویر (PNG, JPG, WEBP)
  • +
  • تشخیص جداول و ساختارهای بصری در اسناد اسکن‌شده
  • +
  • حذف نیاز به کتابخانه‌های سنتی OCR مانند Tesseract
  • +
+
+
+

🚫 Uncensored & Heretic

+
    +
  • بدون فیلترهای اخلاقی سخت‌گیرانه
  • +
  • استخراج دقیق محتوا بدون "مoralizing" یا رد درخواست برای اسناد حساس
  • +
  • ایده‌آل برای پردازش اسناد حقوقی، پزشکی یا فنی خام
  • +
+
+
+

⚡ IMATRIX & MTP

+
    +
  • بهینه‌سازی شده برای سرعت استنتاج (Inference) بالاتر
  • +
  • پشتیبانی از Context Window گسترده (معمولاً 8K تا 32K توکن)
  • +
  • مناسب برای پردازش اسناد چند صفحه‌ای
  • +
+
+
+
+ + +
+

🎯 گام ۲: استراتژی استخراج جهانی (Universal Extraction)

+

به جای استفاده از Extractor های جداگانه و پیچیده برای هر فرمت، یک مسیر هوشمند ترکیبی طراحی می‌کنیم:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
نوع فایلاستراتژی استخراجمسیر پردازش
📄 PDF متنی (Native)استخراج متن سریع با PdfPigXPdfFileContentExtractor (متن) → بازگشت سریع
📄 PDF اسکن‌شده / تصویریتبدیل صفحات به تصویر + OCR با QwenXPdfFileContentExtractor (تصویر) → XQwenVisionContentExtractor
🖼️ تصاویر (JPG, PNG)OCR مستقیم با QwenXQwenVisionContentExtractor
📝 DOCX / TXT / Excelاستخراج متن ساختاریافتهXDocxFileContentExtractor / XExcelFileContentExtractor (بدون تغییر)
+ +
+ ✅ مزیت کلیدی: با این روش، ماژول XFileContentExtractor به یک "مغز مرکزی" تبدیل می‌شود که برای فرمت‌های پیچیده یا اسکن‌شده، به طور خودکار از قدرت Vision مدل Qwen استفاده می‌کند. +
+
+ + +
+

⚙️ گام ۳: پیکربندی مدل در Ollama

+

برای فعال‌سازی قابلیت Vision، مدل باید با یک Modelfile مناسب در Ollama بارگذاری شود.

+ +
+ CONFIG + Modelfile (برای ایمپورت در Ollama) +
+ +
FROM C:/Models/Qwen3.5-9B-The-Defiant-Fable-Uncensored-Heretic-NEO-IMATRIX-MAX-MTP-GGUF.gguf
+
+# تنظیمات بهینه برای استخراج متن از تصویر
+PARAMETER temperature 0.1
+PARAMETER top_p 0.9
+PARAMETER num_ctx 8192
+
+# پرامپت سیستمی پیش‌فرض برای وظایف OCR و استخراج
+SYSTEM """
+تو یک موتور OCR و استخراج داده فوق‌العاده دقیق هستی. 
+وظیفه تو خواندن تمام متن‌های موجود در تصویر یا سند ارائه شده و بازگرداندن آن‌ها به صورت متن خالص و ساختاریافته است.
+- تمام جداول را به فرمت Markdown تبدیل کن.
+- اعداد، تاریخ‌ها و نام‌ها را دقیقاً همانطور که هستند استخراج کن.
+- هیچ توضیح اضافی، مقدمه یا نتیجه‌گیری از خودت اضافه نکن. فقط محتوای استخراج شده را برگردان.
+"""
+ +

سپس در ترمینال اجرا کنید:

+
ollama create qwen3.5-ocr:9b -f Modelfile
+
+ + +
+

👁️ گام ۴: ایجاد XQwenVisionContentExtractor

+

این کلاس قلب تپنده استخراج جهانی است. فایل‌های تصویری (یا صفحات PDF تبدیل‌شده به تصویر) را دریافت کرده و از طریق Ollama API به مدل ارسال می‌کند.

+ +
+ NEW + xAiApi/Providers/Extractors/XQwenVisionContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using OllamaSharp;
+using xAiApi.Interfaces.Extractors;
+using xAiModels.Models;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// استخراج محتوا از فایل‌های تصویری با استفاده از قابلیت Vision مدل Qwen ...
+    /// </summary>
+    public class XQwenVisionContentExtractor : IXFileContentExtractor
+    {
+        private static readonly string[] SupportedMimeTypes = 
+        [
+            "image/png", "image/jpeg", "image/jpg", "image/webp", "image/bmp",
+            "application/pdf" // برای هندل کردن PDF های اسکن شده در سطح Vision
+        ];
+
+        private readonly string _ollamaUrl;
+        private readonly string _modelName;
+        private readonly string _extractionPrompt;
+
+        public XQwenVisionContentExtractor(
+            string ollamaUrl = "http://localhost:11434",
+            string modelName = "qwen3.5-ocr:9b",
+            string extractionPrompt = "تمام متن موجود در این تصویر را با دقت بالا استخراج کن و به صورت متن خالص برگردان.")
+        {
+            _ollamaUrl = ollamaUrl;
+            _modelName = modelName;
+            _extractionPrompt = extractionPrompt;
+        }
+
+        public bool CanExtract(string mimeType)
+        {
+            return SupportedMimeTypes.Contains(mimeType?.ToLowerInvariant() ?? string.Empty);
+        }
+
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default)
+        {
+            var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken);
+            return result.Text;
+        }
+
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default)
+        {
+            var result = new XFileExtractionResult
+            {
+                FileName = fileName,
+                MimeType = mimeType
+            };
+
+            try
+            {
+                // ۱. خواندن استریم به صورت بایت (برای ارسال به مدل Vision)
+                using var memoryStream = new MemoryStream();
+                await fileStream.CopyToAsync(memoryStream, cancellationToken);
+                var imageBytes = memoryStream.ToArray();
+
+                // ۲. تنظیم کلاینت Ollama
+                var ollamaClient = new OllamaApiClient(new Uri(_ollamaUrl), _modelName);
+
+                // ۳. ساخت پیام چندوجهی (Text + Image)
+                var messages = new[]
+                {
+                    new ChatMessage(ChatRole.User, new[]
+                    {
+                        new TextContent(_extractionPrompt),
+                        new DataContent(imageBytes, mimeType) // ارسال تصویر به مدل
+                    })
+                };
+
+                // ۴. فراخوانی مدل برای استخراج متن
+                var response = await ollamaClient.GetResponseAsync(messages, cancellationToken: cancellationToken);
+
+                if (!string.IsNullOrWhiteSpace(response.Text))
+                {
+                    result.Text = response.Text.Trim();
+                    result.IsValid = true;
+                }
+                else
+                {
+                    result.ErrorMessage = "مدل پاسخی برای استخراج تولید نکرد.";
+                }
+            }
+            catch (Exception ex)
+            {
+                result.ErrorMessage = $"خطا در استخراج Vision: {ex.Message}";
+            }
+
+            return result;
+        }
+    }
+}
+ +
+ 💡 نکته فنی: کلاس DataContent از Microsoft.Extensions.AI به طور خودکار بایت‌های تصویر را به فرمت Base64 مناسب برای API مدل‌های Vision تبدیل می‌کند. +
+
+ + +
+

📄 گام ۵: به‌روزرسانی XPdfFileContentExtractor

+

اکنون منطق Fallback را تغییر می‌دهیم. به جای اینکه فقط تصویر را ذخیره کنیم، آن تصویر را به XQwenVisionContentExtractor می‌فرستیم تا متن را استخراج کند.

+ +
+ MODIFY + xAiApi/Providers/Extractors/XPdfFileContentExtractor.cs +
+ +
using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using UglyToad.PdfPig;
+using xAiApi.Interfaces.Extractors;
+using xAiModels.Models;
+
+namespace xAiApi.Providers.Extractors
+{
+    public class XPdfFileContentExtractor : IXFileContentExtractor
+    {
+        private const int MinTextLengthThreshold = 100;
+        private const int MaxPagesForImageFallback = 5; // محدود کردن برای جلوگیری از مصرف بیش از حد GPU
+
+        private readonly IXFileContentExtractor _visionExtractor;
+
+        // تزریق Vision Extractor از طریق Constructor
+        public XPdfFileContentExtractor(IXFileContentExtractor visionExtractor)
+        {
+            _visionExtractor = visionExtractor;
+        }
+
+        public bool CanExtract(string mimeType) => mimeType?.ToLowerInvariant() == "application/pdf";
+
+        public async Task<string> ExtractAsync(Stream fileStream, string mimeType, CancellationToken cancellationToken = default)
+        {
+            var result = await ExtractRichAsync(fileStream, "document.pdf", mimeType, cancellationToken);
+            return result.Text;
+        }
+
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default)
+        {
+            var result = new XFileExtractionResult { FileName = fileName, MimeType = mimeType };
+
+            using var memoryStream = new MemoryStream();
+            await fileStream.CopyToAsync(memoryStream, cancellationToken);
+            
+            // مرحله ۱: تلاش برای استخراج متن معمولی
+            memoryStream.Position = 0;
+            var extractedText = await ExtractTextAsync(memoryStream, cancellationToken);
+
+            if (IsTextSufficient(extractedText))
+            {
+                result.Text = extractedText;
+                result.IsValid = true;
+                return result;
+            }
+
+            // مرحله ۲: Fallback به Vision OCR
+            memoryStream.Position = 0;
+            result = await FallbackToVisionOcrAsync(memoryStream, fileName, cancellationToken);
+            result.UsedImageFallback = true;
+
+            return result;
+        }
+
+        private async Task<string> ExtractTextAsync(Stream pdfStream, CancellationToken cancellationToken)
+        {
+            return await Task.Run(() =>
+            {
+                var sb = new StringBuilder();
+                using var document = PdfDocument.Open(pdfStream);
+                foreach (var page in document.GetPages())
+                {
+                    sb.AppendLine(page.Text?.Trim());
+                }
+                return sb.ToString();
+            }, cancellationToken);
+        }
+
+        private bool IsTextSufficient(string text)
+        {
+            if (string.IsNullOrWhiteSpace(text)) return false;
+            var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray());
+            return cleanText.Length >= MinTextLengthThreshold;
+        }
+
+        private async Task<XFileExtractionResult> FallbackToVisionOcrAsync(
+            Stream pdfStream,
+            string fileName,
+            CancellationToken cancellationToken)
+        {
+            var result = new XFileExtractionResult { FileName = fileName, MimeType = "application/pdf", UsedImageFallback = true };
+            var sb = new StringBuilder();
+
+            await Task.Run(() =>
+            {
+                using var document = PdfDocument.Open(pdfStream);
+                var pageCount = Math.Min(document.NumberOfPages, MaxPagesForImageFallback);
+
+                for (int i = 0; i < pageCount; i++)
+                {
+                    // تبدیل صفحه به تصویر (با استفاده از PdfPig یا Pdfium)
+                    // نکته: برای سادگی، فرض می‌کنیم متدی داریم که صفحه را به Stream تصویر تبدیل می‌کند
+                    // در پیاده‌سازی واقعی از PdfiumViewer.RenderPage استفاده کنید (همانند کد قبلی)
+                    using var pageImageStream = RenderPageToImageStream(document, i);
+                    
+                    // ارسال تصویر به Qwen Vision Extractor
+                    var ocrResult = _visionExtractor.ExtractRichAsync(
+                        pageImageStream, 
+                        $"{fileName}_page_{i+1}", 
+                        "image/png", 
+                        cancellationToken).GetAwaiter().GetResult();
+
+                    if (ocrResult.IsValid)
+                    {
+                        sb.AppendLine($"--- Page {i + 1} ---");
+                        sb.AppendLine(ocrResult.Text);
+                    }
+                }
+            }, cancellationToken);
+
+            result.Text = sb.ToString();
+            result.IsValid = !string.IsNullOrWhiteSpace(result.Text);
+            return result;
+        }
+
+        // Placeholder: باید با منطق PdfiumViewer که قبلاً نوشتید جایگزین شود
+        private Stream RenderPageToImageStream(dynamic document, int pageIndex)
+        {
+            // پیاده‌سازی RenderPage به MemoryStream از PdfiumViewer
+            throw new NotImplementedException("از منطق PdfiumViewer.RenderPage استفاده کنید");
+        }
+    }
+}
+
+ + +
+

🔌 گام ۶: ثبت سرویس‌ها در Dependency Injection

+

اکنون باید زنجیره Extractor ها را در Startup.cs به درستی متصل کنیم.

+ +
+ MODIFY + xAiApi/Startup.cs (متد ConfigureServices) +
+ +
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (ثبت‌های قبلی)
+
+    // ۱. ثبت Vision Extractor به صورت Singleton (چون State-less است)
+    services.AddSingleton<IXFileContentExtractor>(sp => 
+        new XQwenVisionContentExtractor(
+            ollamaUrl: "http://localhost:11434",
+            modelName: "qwen3.5-ocr:9b"
+        ));
+
+    // ۲. ثبت PDF Extractor و تزریق Vision Extractor به آن
+    services.AddSingleton<IXFileContentExtractor>(sp => 
+    {
+        var visionExtractor = sp.GetRequiredService<IXFileContentExtractor>(); 
+        // نکته: برای جلوگیری از تداخل در Resolve، بهتر است Vision Extractor را با نام/interface خاص ثبت کنید 
+        // یا مستقیماً اینstantiate کنید:
+        var specificVisionExtractor = new XQwenVisionContentExtractor();
+        return new XPdfFileContentExtractor(specificVisionExtractor);
+    });
+
+    // ۳. ثبت سایر Extractor ها
+    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
+
+    // ۴. ثبت Composite Extractor (این کلاس به طور خودکار همه IXFileContentExtractor های ثبت شده را جمع‌آوری می‌کند)
+    services.AddSingleton<XFileContentExtractor>();
+    
+    // اطمینان از اینکه سرویس اصلی از نوع Composite است
+    services.AddSingleton<IXFileContentExtractor>(sp => sp.GetRequiredService<XFileContentExtractor>());
+
+    // ... (بقیه کدها)
+}
+
+ + +
+

📝 گام ۷: مهندسی پرامپت برای OCR دقیق

+

کیفیت استخراج مستقیماً به پرامپت ارسال شده به مدل Qwen بستگی دارد. این پرامپت‌ها را در appsettings.json یا کد تعریف کنید:

+ +
+
+

🎯 پرامپت استخراج عمومی (General OCR)

+

+ "تمام متن موجود در این تصویر را با دقت کاراکتر به کاراکتر استخراج کن. ساختار پاراگراف‌ها را حفظ کن. اگر جدولی وجود دارد، آن را به فرمت Markdown تبدیل کن. هیچ توضیح اضافی نده." +

+
+
+

📊 پرامپت استخراج داده ساختاریافته (Structured)

+

+ "این تصویر یک فاکتور/سند است. فقط موارد زیر را استخراج و به صورت JSON برگردان: {\"issuer\": \"\", \"date\": \"\", \"total_amount\": \"\", \"items\": []}. اگر موردی یافت نشد، null بگذار." +

+
+
+
+ + +
+

⚠️ گام ۸: ملاحظات عملکردی و Production

+ + + + + + + + + + + + + + + + + + + + + + +
چالشراه‌حل پیشنهادی در معماری
محدودیت Context Windowدر XPdfFileContentExtractor، MaxPagesForImageFallback را روی ۵ تا ۱۰ تنظیم کنید تا مدل Overload نشود.
زمان پاسخ‌دهی (Latency)پردازش Vision زمان‌بر است. در AskAsync از Timeout مناسب (مثلاً ۶۰ ثانیه) در HttpClient استفاده کنید.
مصرف حافظه GPUمدل 9B حدود ۶-۸ گیگابایت VRAM نیاز دارد. اطمینان حاصل کنید سرور Ollama منابع کافی دارد. از پردازش همزمان بیش از ۲ فایل بزرگ خودداری کنید.
کیفیت OCR زبان فارسیمدل‌های Qwen در فارسی خوب عمل می‌کنند، اما برای اسناد بسیار قدیمی یا با کیفیت پایین، ممکن است نیاز به پیش‌پردازش تصویر (افزایش کنتراست) باشد.
+ +
+ ⚠️ نکته حیاتی درباره XFileContentExtractor فعلی:
+ کد فعلی XFileContentExtractor از Reflection برای یافتن Extractor ها استفاده می‌کند. برای اطمینان از عملکرد صحیح، اطمینان حاصل کنید که XQwenVisionContentExtractor و XPdfFileContentExtractor در همان Assembly (پروژه xAiApi) کامپایل شده‌اند تا توسط Reflection شناسایی شوند. +
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/Docs/Developer/9.html b/Documents/Docs/Developer/9.html new file mode 100644 index 0000000..2eeb113 --- /dev/null +++ b/Documents/Docs/Developer/9.html @@ -0,0 +1,385 @@ + + + + + +بازطراحی XFileContentExtractor با استفاده از تزریق وابستگی - فن آوران ساحر علم + + + +
+ +
+

🔄 بازطراحی XFileContentExtractor بر پایه DI

+
حذف Reflection و جایگزینی با تزریق وابستگی استاندارد برای مدیریت Extractor ها
+
+ +
+
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
+
🏢 شرکت: فن آوران ساحر علم
+
📅 تاریخ: شنبه ۱۲ مهر ۱۴۰۵
+
📦 پروژه: xAiApi
+
+ +
+ + + + +
+

🔍 گام ۱: تحلیل نواقص کد فعلی

+

در نسخه فعلی XFileContentExtractor، از مکانیزم Reflection (Assembly.LoadFrom و Activator.CreateInstance) برای یافتن و ساخت نمونه‌های Extractor استفاده شده است. این رویکرد دارای نواقص جدی زیر است:

+ +
+
+

❌ عدم پشتیبانی از وابستگی‌ها (DI Bypass)

+

کلاس‌هایی مانند XVisionFileContentExtractor یا XAudioFileContentExtractor دارای وابستگی‌هایی مانند IXDefaultAIOCRService یا XAiApiConfiguration هستند. Activator.CreateInstance نمی‌تواند این وابستگی‌ها را حل کند و باعث خطای MissingMethodException می‌شود.

+
+
+

❌ مشکل قفل‌شدگی فایل (File Locking)

+

استفاده از Assembly.LoadFrom در حلقه روی فایل‌های DLL می‌تواند باعث قفل شدن فایل‌ها در محیط‌های هاستینگ (مانند IIS) و جلوگیری از به‌روزرسانی یا Deploy شود.

+
+
+

❌ خطر حلقه بی‌نهایت (Circular Dependency)

+

اگر XFileContentExtractor خودش به عنوان IXFileContentExtractor در DI ثبت شود، ممکن است در لیست بازگردانده شود و باعث فراخوانی بازگشتی بی‌نهایت در متدهای CanExtract شود.

+
+
+ +
+ ⚠️ نتیجه‌گیری: استفاده از Reflection برای ساخت اشیایی که وابستگی دارند، یک Anti-Pattern در .NET Core است. راه‌حل استاندارد، استفاده از قابلیت IEnumerable<T> در تزریق وابستگی است. +
+
+ + +
+

🛠️ گام ۲: بازنویسی کلاس XFileContentExtractor

+

به جای اسکن دستی DLL ها، از کانتینر DI می‌خواهیم تمام پیاده‌سازی‌های ثبت‌شده‌ی IXFileContentExtractor را به ما تزریق کند. سپس خودِ کامپوزیت را از لیست فیلتر می‌کنیم.

+ +
+ MODIFY + xAiApi/Providers/Extractors/XFileContentExtractor.cs +
+ +
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using xAiApi.Interfaces.Extractors;
+using xAiModels.Models;
+using xCommons.Extensions;
+using xExceptions.Constants;
+
+namespace xAiApi.Providers.Extractors
+{
+    /// <summary>
+    /// Composite extractor that delegates to appropriate extractor
+    /// based on MIME type using Dependency Injection ...
+    /// </summary>
+    public class XFileContentExtractor : IXFileContentExtractor
+    {
+        private readonly IList<IXFileContentExtractor> extractors;
+
+        /// <summary>
+        /// Constructor: Inject all registered IXFileContentExtractor instances ...
+        /// </summary>
+        public XFileContentExtractor(IEnumerable<IXFileContentExtractor> availableExtractors)
+        {
+            // فیلتر کردن خودِ این کلاس برای جلوگیری از حلقه بی‌نهایت (Circular Dependency)
+            extractors = availableExtractors
+                .Where(e => e.GetType() != typeof(XFileContentExtractor))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Check if any registered extractor supports the specified MIME type ...
+        /// </summary>
+        public bool CanExtract(string mimeType)
+        {
+            return extractors.Any(e => e.CanExtract(mimeType));
+        }
+
+        /// <summary>
+        /// Extract text content from file stream ...
+        /// </summary>
+        public async Task<string> ExtractAsync(
+            Stream fileStream,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var extractor = extractors.FirstOrDefault(e => e.CanExtract(mimeType));
+            
+            if (extractor.IsNull())
+            {
+                XException.NotAllowed.Throw($"Unsupported file type: {mimeType}");
+            }
+
+            return await extractor.ExtractAsync(
+                fileStream: fileStream,
+                mimeType: mimeType,
+                cancellationToken: cancellationToken
+            );
+        }
+
+        /// <summary>
+        /// Extract content from stream as Rich Result ...
+        /// </summary>
+        public async Task<XFileExtractionResult> ExtractRichAsync(
+            Stream fileStream,
+            string fileName,
+            string mimeType,
+            CancellationToken cancellationToken = default
+        )
+        {
+            var extractor = extractors.FirstOrDefault(e => e.CanExtract(mimeType));
+            
+            if (extractor.IsNull())
+            {
+                XException.NotAllowed.Throw($"Unsupported file type: {mimeType}");
+            }
+
+            return await extractor.ExtractRichAsync(
+                fileStream: fileStream,
+                fileName: fileName,
+                mimeType: mimeType,
+                cancellationToken: cancellationToken
+            );
+        }
+    }
+}
+
+ + +
+

🔌 گام ۳: ثبت صحیح سرویس‌ها در Startup.cs

+

برای اینکه تزریق IEnumerable<IXFileContentExtractor> کار کند، باید تمام Extractor های خاص را به صورت Singleton (چون State-less هستند) در کانتینر DI ثبت کنیم. کانتینر .NET Core به طور خودکار آن‌ها را در یک لیست جمع‌آوری می‌کند.

+ +
+ MODIFY + xAiApi/Startup.cs (متد ConfigureServices) +
+ +
public void ConfigureServices(IServiceCollection services)
+{
+    // ... (ثبت‌های قبلی سرویس‌ها)
+
+    // ==========================================================
+    // ✅ ثبت File Content Extractors (الگوی Composite)
+    // ==========================================================
+    
+    // ۱. ثبت Extractor های پایه
+    services.AddSingleton<IXFileContentExtractor, XPlainTextFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XDocxFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XExcelFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XPdfFileContentExtractor>();
+    
+    // ۲. ثبت Extractor های پیشرفته (تصویر و صوت)
+    // نکته: XImageFileContentExtractor و XAudioFileContentExtractor باید در پروژه موجود باشند
+    services.AddSingleton<IXFileContentExtractor, XImageFileContentExtractor>();
+    services.AddSingleton<IXFileContentExtractor, XAudioFileContentExtractor>();
+    
+    // ۳. ثبت Vision Extractor (که وابستگی به IXDefaultAIOCRService دارد)
+    // این خط به طور خودکار وابستگی‌های XVisionFileContentExtractor را از DI حل می‌کند
+    services.AddSingleton<IXFileContentExtractor, XVisionFileContentExtractor>();
+
+    // ۴. ثبت کامپوزیت اصلی (این کلاس لیست بالا را در Constructor دریافت می‌کند)
+    services.AddSingleton<IXFileContentExtractor, XFileContentExtractor>();
+
+    // ... (ثبت سرویس‌های AI و سایر موارد)
+    services.AddScoped<IXDefaultAiService, XDefaultAiService>();
+    services.AddScoped<IXDefaultAIOCRService, XDefaultAIOCRService>();
+    services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
+    services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
+}
+ +
+ 💡 نکته حیاتی درباره ترتیب ثبت:
+ در .NET Core، وقتی IEnumerable<T> را Inject می‌کنید، تمام ثبت‌های T (شامل خودِ XFileContentExtractor اگر قبل از فیلتر کردن باشد) را برمی‌گرداند. به همین دلیل در Constructor کلاس XFileContentExtractor، خط .Where(e => e.GetType() != typeof(XFileContentExtractor)) اضافه شده است تا از حلقه بی‌نهایت جلوگیری شود. +
+
+ + +
+

✨ گام ۴: مزایای رویکرد جدید

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ویژگیرویکرد قدیمی (Reflection)رویکرد جدید (DI)
حل وابستگی‌ها (Dependencies)❌ شکست می‌خورد✅ به طور خودکار حل می‌شود
عملکرد (Performance)⚠️ کند (اسکن DLL در هر بار)✅ بسیار سریع (Resolved at startup)
قابلیت تست (Unit Testing)❌ بسیار دشوار (Mocking سخت)✅ آسان (تزریق لیست Mock)
پایداری در محیط Production❌ خطر File Locking✅ کاملاً پایدار و استاندارد
افزودن Extractor جدیدخودکار (اما با ریسک)فقط افزودن یک خط AddSingleton در Startup.cs
+ +
+ ✅ نتیجه‌گیری نهایی:
+ با این تغییر، معماری پروژه شما کاملاً با اصول SOLID (به ویژه Dependency Inversion) و الگوهای استاندارد .NET Core همسو می‌شود. کلاس XVisionFileContentExtractor که به IXDefaultAIOCRService وابسته است، اکنون بدون هیچ خطایی مقداردهی اولیه خواهد شد. +
+
+ +
+ + + +
+ + \ No newline at end of file diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_103219.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_103219.txt new file mode 100644 index 0000000..f2aee21 --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_103219.txt @@ -0,0 +1,5752 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + // services.AddSingleton(); + // services.AddSingleton(); + services.AddSingleton(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + public const string XAiApiIntroductionPromptName = "Introduction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System.Linq; +using xAiApi.Constants; +using xAiModels.Models; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using System.Collections.Generic; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003044828_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url)) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using xAiModels.Models; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Reflection; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; +using xAiModels.Models; + +namespace xAiApi.Providers +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList extractors = []; + + public XFileContentExtractor() + { + // + var path = Path.Combine(AppContext.BaseDirectory); + var files = Directory.GetFiles(path, "*.dll"); + foreach (var dll in files) + { + // + // Retrieve DLL Assembly ... + var assembly = Assembly.LoadFrom(dll); + + // + // Extract Registerar Types ... + var extractorTypes = assembly + .GetExportedTypes() + .Where(t => + !t.IsAbstract && + typeof(IXFileContentExtractor).IsAssignableFrom(t)); + foreach (var extractorType in extractorTypes) + { + // + // Ignore XFileContentExtractor ... + if (extractorType.FullName.Contains(nameof(XFileContentExtractor))) + { + continue; + } + + // + // Create Instance of Service ... + var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType); + if (!instance.IsNull()) + { + // + // Add Registerar Instance to Collection ... + extractors.Add(instance); + } + } + } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + return extractors.Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_114330.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_114330.txt new file mode 100644 index 0000000..df07e4d --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_114330.txt @@ -0,0 +1,5908 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + // services.AddSingleton(); + // services.AddSingleton(); + services.AddSingleton(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System.Linq; +using xAiApi.Constants; +using xAiModels.Models; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using System.Collections.Generic; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAIOCRService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAIOCRService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003044828_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url)) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAIOCRService.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace xAiApi.Providers +{ + public class XDefaultAIOCRService : XAIServiceBase, IXDefaultAIOCRService + { + + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Reflection; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList extractors = []; + + public XFileContentExtractor() + { + // + var path = Path.Combine(AppContext.BaseDirectory); + var files = Directory.GetFiles(path, "*.dll"); + foreach (var dll in files) + { + // + // Retrieve DLL Assembly ... + var assembly = Assembly.LoadFrom(dll); + + // + // Extract Registerar Types ... + var extractorTypes = assembly + .GetExportedTypes() + .Where(t => + !t.IsAbstract && + typeof(IXFileContentExtractor).IsAssignableFrom(t)); + foreach (var extractorType in extractorTypes) + { + // + // Ignore XFileContentExtractor ... + if (extractorType.FullName.Contains(nameof(XFileContentExtractor))) + { + continue; + } + + // + // Create Instance of Service ... + var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType); + if (!instance.IsNull()) + { + // + // Add Registerar Instance to Collection ... + extractors.Add(instance); + } + } + } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + return extractors.Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf" + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + } + } +} diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_141948.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_141948.txt new file mode 100644 index 0000000..21cc2d0 --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_141948.txt @@ -0,0 +1,6283 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using xAiApi.Providers.Extractors; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + services.AddSingleton(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + public async Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) { + // + try + { + // + // Validate ... + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = string.Empty; + + // + + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System.Linq; +using xAiApi.Constants; +using xAiModels.Models; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using System.Collections.Generic; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAIOCRService.cs + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAIOCRService : IXAiServiceBase + { + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ); + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003044828_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url)) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public virtual async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public virtual async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public virtual async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public virtual async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public virtual async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public virtual async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + public async Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var extractionResults = new List(); + foreach (var file in files) + { + // + if (file.IsNull() || file.Length == 0) + { + continue; + } + + // + var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; + if (!fileContentExtractor.CanExtract(mimeType)) + { + continue; + } + + // + try + { + // + using var stream = file.OpenReadStream(); + + // + var fr = await fileContentExtractor.ExtractRichAsync( + fileStream: stream, + mimeType: mimeType, + fileName: file.FileName, + cancellationToken: cancellationToken + ); + + // + if (fr.IsValid()) + { + extractionResults.Add(fr); + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + continue; + } + } + + // + var result = string.Empty; + if (extractionResults.HasChild()) + { + // + extractionResults.ForEach(er => + { + result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; + }); + } + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAIOCRService.cs + +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + public class XDefaultAIOCRService : XAIServiceBase, IXDefaultAIOCRService + { + private readonly string prompt; + + public XDefaultAIOCRService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null, + string prompt = null + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + model + ) + { + // + this.prompt = prompt; + if (this.prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + } + + // + #region OCR ... + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + public virtual async Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var response = await client.GetResponseAsync( + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: messages + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.EMMA; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor() : this( + language: "fa", + whisperModelPath: "Models/ggml-base.bin" + ) + { } + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Reflection; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList extractors = []; + + public XFileContentExtractor() + { + // + var path = Path.Combine(AppContext.BaseDirectory); + var files = Directory.GetFiles(path, "*.dll"); + foreach (var dll in files) + { + // + // Retrieve DLL Assembly ... + var assembly = Assembly.LoadFrom(dll); + + // + // Extract Registerar Types ... + var extractorTypes = assembly + .GetExportedTypes() + .Where(t => + !t.IsAbstract && + typeof(IXFileContentExtractor).IsAssignableFrom(t)); + foreach (var extractorType in extractorTypes) + { + // + // Ignore XFileContentExtractor ... + if (extractorType.FullName.Contains(nameof(XFileContentExtractor))) + { + continue; + } + + // + // Create Instance of Service ... + var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType); + if (!instance.IsNull()) + { + // + // Add Registerar Instance to Collection ... + extractors.Add(instance); + } + } + } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + return extractors.Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor() : this(enableOcrFallback: true) + { } + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf" + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + private readonly IXDefaultAIOCRService aIOCRService; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + IXDefaultAIOCRService aIOCRService, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + this.aIOCRService = aIOCRService; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + var messages = new[] + { + // + new ChatMessage(ChatRole.User, new[] + { + new DataContent(imageBytes, mimeType) + }) + }; + + // + var extractedContent = await aIOCRService.RequestOCRAsync( + messages: messages, + cancellationToken: cancellationToken + ); + if (!extractedContent.IsNullOrEmpty()) + { + result.Text = extractedContent.Trim(); + } + else + { + result.ErrorMessage = "there is not any Extracted Content ..."; + } + } + catch (Exception ex) + { + result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ..."; + } + + // + return result; + } + } +} diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_144445.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_144445.txt new file mode 100644 index 0000000..9c51a7f --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_144445.txt @@ -0,0 +1,6453 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using xAiApi.Providers.Extractors; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // + services.AddSingleton(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + public async Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) { + // + try + { + // + // Validate ... + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = string.Empty; + + // + + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System.Linq; +using xAiApi.Constants; +using xAiModels.Models; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using System.Collections.Generic; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAIOCRService.cs + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using xAiModels.Models; +using System.Net.Http; + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAIOCRService + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Prompt ... + /// + string Prompt { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ); + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + CancellationToken cancellationToken = default + ); + + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003044828_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url)) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public virtual async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public virtual async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public virtual async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public virtual async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public virtual async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public virtual async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + public async Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var extractionResults = new List(); + foreach (var file in files) + { + // + if (file.IsNull() || file.Length == 0) + { + continue; + } + + // + var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; + if (!fileContentExtractor.CanExtract(mimeType)) + { + continue; + } + + // + try + { + // + using var stream = file.OpenReadStream(); + + // + var fr = await fileContentExtractor.ExtractRichAsync( + fileStream: stream, + mimeType: mimeType, + fileName: file.FileName, + cancellationToken: cancellationToken + ); + + // + if (fr.IsValid()) + { + extractionResults.Add(fr); + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + continue; + } + } + + // + var result = string.Empty; + if (extractionResults.HasChild()) + { + // + extractionResults.ForEach(er => + { + result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; + }); + } + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAIOCRService.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + public class XDefaultAIOCRService : IXDefaultAIOCRService + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Prompt ... + /// + public string Prompt { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + public XDefaultAIOCRService( + XAiApiConfiguration configuration, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string model = null, + string prompt = null + ) + { + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + Prompt = prompt; + if (Prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region OCR ... + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + public virtual async Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + Prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var response = await client.GetResponseAsync( + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + Prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: messages + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + #endregion + + // + #region Preaprations ... + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.EMMA; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor() : this( + language: "fa", + whisperModelPath: "Models/ggml-base.bin" + ) + { } + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Reflection; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList extractors = []; + + public XFileContentExtractor( + IEnumerable availableExtractors + ) + { + // + extractors = [.. availableExtractors + .Where(e => e.GetType() != typeof(XFileContentExtractor))]; + + // // + // var path = Path.Combine(AppContext.BaseDirectory); + // var files = Directory.GetFiles(path, "*.dll"); + // foreach (var dll in files) + // { + // // + // // Retrieve DLL Assembly ... + // var assembly = Assembly.LoadFrom(dll); + + // // + // // Extract Registerar Types ... + // var extractorTypes = assembly + // .GetExportedTypes() + // .Where(t => + // !t.IsAbstract && + // typeof(IXFileContentExtractor).IsAssignableFrom(t)); + // foreach (var extractorType in extractorTypes) + // { + // // + // // Ignore XFileContentExtractor ... + // if (extractorType.FullName.Contains(nameof(XFileContentExtractor))) + // { + // continue; + // } + + // // + // // Create Instance of Service ... + // var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType); + // if (!instance.IsNull()) + // { + // // + // // Add Registerar Instance to Collection ... + // extractors.Add(instance); + // } + // } + // } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + return extractors.Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor() : this(enableOcrFallback: true) + { } + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf" + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + private readonly IXDefaultAIOCRService aIOCRService; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + IXDefaultAIOCRService aIOCRService, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + this.aIOCRService = aIOCRService; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + var messages = new[] + { + // + new ChatMessage(ChatRole.User, new[] + { + new DataContent(imageBytes, mimeType) + }) + }; + + // + var extractedContent = await aIOCRService.RequestOCRAsync( + messages: messages, + cancellationToken: cancellationToken + ); + if (!extractedContent.IsNullOrEmpty()) + { + result.Text = extractedContent.Trim(); + } + else + { + result.ErrorMessage = "there is not any Extracted Content ..."; + } + } + catch (Exception ex) + { + result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ..."; + } + + // + return result; + } + } +} diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_145153.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_145153.txt new file mode 100644 index 0000000..1fdd1a6 --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_145153.txt @@ -0,0 +1,6455 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using xAiApi.Providers.Extractors; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // + services.AddScoped(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + public async Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) { + // + try + { + // + // Validate ... + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = string.Empty; + + // + + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System.Linq; +using xAiApi.Constants; +using xAiModels.Models; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using System.Collections.Generic; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAIOCRService.cs + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using xAiModels.Models; +using System.Net.Http; + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAIOCRService + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Prompt ... + /// + string Prompt { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ); + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + CancellationToken cancellationToken = default + ); + + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003044828_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url)) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public virtual async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public virtual async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public virtual async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public virtual async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public virtual async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public virtual async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + public async Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var extractionResults = new List(); + foreach (var file in files) + { + // + if (file.IsNull() || file.Length == 0) + { + continue; + } + + // + var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; + if (!fileContentExtractor.CanExtract(mimeType)) + { + continue; + } + + // + try + { + // + using var stream = file.OpenReadStream(); + + // + var fr = await fileContentExtractor.ExtractRichAsync( + fileStream: stream, + mimeType: mimeType, + fileName: file.FileName, + cancellationToken: cancellationToken + ); + + // + if (fr.IsValid()) + { + extractionResults.Add(fr); + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + continue; + } + } + + // + var result = string.Empty; + if (extractionResults.HasChild()) + { + // + extractionResults.ForEach(er => + { + result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; + }); + } + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAIOCRService.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + public class XDefaultAIOCRService : IXDefaultAIOCRService + { + private readonly ILogger logger; + + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Prompt ... + /// + public string Prompt { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + public XDefaultAIOCRService( + XAiApiConfiguration configuration, + ILogger logger, + string model = null, + string prompt = null + ) + { + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.logger = logger; + + // + Prompt = prompt; + if (Prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region OCR ... + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + public virtual async Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + Prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var response = await client.GetResponseAsync( + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + Prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: messages + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + #endregion + + // + #region Preaprations ... + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.EMMA; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor() : this( + language: "fa", + whisperModelPath: "Models/ggml-base.bin" + ) + { } + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Reflection; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList extractors = []; + + public XFileContentExtractor( + IEnumerable availableExtractors + ) + { + // + extractors = [.. availableExtractors + .Where(e => e.GetType() != typeof(XFileContentExtractor))]; + + // // + // var path = Path.Combine(AppContext.BaseDirectory); + // var files = Directory.GetFiles(path, "*.dll"); + // foreach (var dll in files) + // { + // // + // // Retrieve DLL Assembly ... + // var assembly = Assembly.LoadFrom(dll); + + // // + // // Extract Registerar Types ... + // var extractorTypes = assembly + // .GetExportedTypes() + // .Where(t => + // !t.IsAbstract && + // typeof(IXFileContentExtractor).IsAssignableFrom(t)); + // foreach (var extractorType in extractorTypes) + // { + // // + // // Ignore XFileContentExtractor ... + // if (extractorType.FullName.Contains(nameof(XFileContentExtractor))) + // { + // continue; + // } + + // // + // // Create Instance of Service ... + // var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType); + // if (!instance.IsNull()) + // { + // // + // // Add Registerar Instance to Collection ... + // extractors.Add(instance); + // } + // } + // } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + return extractors.Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor() : this(enableOcrFallback: true) + { } + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf" + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + private readonly IXDefaultAIOCRService aIOCRService; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + IXDefaultAIOCRService aIOCRService, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + this.aIOCRService = aIOCRService; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + var messages = new[] + { + // + new ChatMessage(ChatRole.User, new[] + { + new DataContent(imageBytes, mimeType) + }) + }; + + // + var extractedContent = await aIOCRService.RequestOCRAsync( + messages: messages, + cancellationToken: cancellationToken + ); + if (!extractedContent.IsNullOrEmpty()) + { + result.Text = extractedContent.Trim(); + } + else + { + result.ErrorMessage = "there is not any Extracted Content ..."; + } + } + catch (Exception ex) + { + result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ..."; + } + + // + return result; + } + } +} diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_145822.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_145822.txt new file mode 100644 index 0000000..75c7e05 --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_145822.txt @@ -0,0 +1,6453 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using xAiApi.Providers.Extractors; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // + services.AddSingleton(); + services.AddSingleton(); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + public async Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) { + // + try + { + // + // Validate ... + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = string.Empty; + + // + + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System.Linq; +using xAiApi.Constants; +using xAiModels.Models; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using System.Collections.Generic; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + Task> ExtractContent( + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAIOCRService.cs + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using xAiModels.Models; +using System.Net.Http; + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAIOCRService + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Prompt ... + /// + string Prompt { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ); + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + CancellationToken cancellationToken = default + ); + + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + IChatClient GetClient(); + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + HttpClient GetHttpClient(string url = null); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractor + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003044828_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003044828_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url)) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public virtual async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public virtual async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public virtual async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public virtual async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public virtual async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public virtual async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + public async Task ExtractContent( + IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var extractionResults = new List(); + foreach (var file in files) + { + // + if (file.IsNull() || file.Length == 0) + { + continue; + } + + // + var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; + if (!fileContentExtractor.CanExtract(mimeType)) + { + continue; + } + + // + try + { + // + using var stream = file.OpenReadStream(); + + // + var fr = await fileContentExtractor.ExtractRichAsync( + fileStream: stream, + mimeType: mimeType, + fileName: file.FileName, + cancellationToken: cancellationToken + ); + + // + if (fr.IsValid()) + { + extractionResults.Add(fr); + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + continue; + } + } + + // + var result = string.Empty; + if (extractionResults.HasChild()) + { + // + extractionResults.ForEach(er => + { + result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; + }); + } + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAIOCRService.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + public class XDefaultAIOCRService : IXDefaultAIOCRService + { + private readonly ILogger logger; + + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Prompt ... + /// + public string Prompt { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + public XDefaultAIOCRService( + XAiApiConfiguration configuration, + ILogger logger, + string model = null, + string prompt = null + ) + { + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.logger = logger; + + // + Prompt = prompt; + if (Prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region OCR ... + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + public virtual async Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + Prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var response = await client.GetResponseAsync( + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + Prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: messages + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + #endregion + + // + #region Preaprations ... + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + public virtual IChatClient GetClient() + { + // + // Try to Initialize LLm ... + var model = Descriptor.LLM; + var apiKey = Descriptor.ApiKey; + var url = new Uri(Descriptor.Url); + var httpClient = GetHttpClient(Descriptor.Url); + + // + IChatClient result = null; + switch (Descriptor.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Create Custom HttpClient for Communicating with LLM API ... + /// + /// + /// + public virtual HttpClient GetHttpClient(string url = null) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = url.IsNullOrEmpty() + ? null + : new Uri(url), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.EMMA; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor() : this( + language: "fa", + whisperModelPath: "Models/ggml-base.bin" + ) + { } + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList extractors = []; + + public XFileContentExtractor( + IEnumerable availableExtractors + ) + { + // + extractors = [.. availableExtractors + .Where(e => e.GetType() != typeof(XFileContentExtractor))]; + + // // + // var path = Path.Combine(AppContext.BaseDirectory); + // var files = Directory.GetFiles(path, "*.dll"); + // foreach (var dll in files) + // { + // // + // // Retrieve DLL Assembly ... + // var assembly = Assembly.LoadFrom(dll); + + // // + // // Extract Registerar Types ... + // var extractorTypes = assembly + // .GetExportedTypes() + // .Where(t => + // !t.IsAbstract && + // typeof(IXFileContentExtractor).IsAssignableFrom(t)); + // foreach (var extractorType in extractorTypes) + // { + // // + // // Ignore XFileContentExtractor ... + // if (extractorType.FullName.Contains(nameof(XFileContentExtractor))) + // { + // continue; + // } + + // // + // // Create Instance of Service ... + // var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType); + // if (!instance.IsNull()) + // { + // // + // // Add Registerar Instance to Collection ... + // extractors.Add(instance); + // } + // } + // } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + return extractors.Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var extractor = extractors + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor() : this(enableOcrFallback: true) + { } + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf" + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + private readonly IXDefaultAIOCRService aIOCRService; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + IXDefaultAIOCRService aIOCRService, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + this.aIOCRService = aIOCRService; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + var messages = new[] + { + // + new ChatMessage(ChatRole.User, new[] + { + new DataContent(imageBytes, mimeType) + }) + }; + + // + var extractedContent = await aIOCRService.RequestOCRAsync( + messages: messages, + cancellationToken: cancellationToken + ); + if (!extractedContent.IsNullOrEmpty()) + { + result.Text = extractedContent.Trim(); + } + else + { + result.ErrorMessage = "there is not any Extracted Content ..."; + } + } + catch (Exception ex) + { + result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ..."; + } + + // + return result; + } + } +} diff --git a/Documents/GeneratedContext/xDashboard-xAiApi-20261003_172953.txt b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_172953.txt new file mode 100644 index 0000000..695788d --- /dev/null +++ b/Documents/GeneratedContext/xDashboard-xAiApi-20261003_172953.txt @@ -0,0 +1,6302 @@ + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Program.cs + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xAiApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Startup.cs + +using System; +using System.IO; +using xAiApi.DI; +using System.Linq; +using xAiModels.DI; +using xTagService.DI; +using System.Net.Http; +using xAiApi.Database; +using xHttpService.DI; +using xPushService.DI; +using xFileService.DI; +using xDataService.DI; +using Newtonsoft.Json; +using xAiApi.Providers; +using System.Reflection; +using xAiApi.Extensions; +using xStringService.DI; +using xAiApi.Interfaces; +using xStorageService.DI; +using xIdentityService.DI; +using xDataService.Models; +using xCommons.Extensions; +using xPushService.Helpers; +using xCommons.Configurations; +using System.Collections.Generic; +using xAiApi.Providers.Extractors; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using xAiApi.Interfaces.Extractors; +using Newtonsoft.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using IdentityModel.AspNetCore.OAuth2Introspection; + +namespace xAiApi +{ + public class Startup + { + // + #region Props ... + public IConfiguration Configuration { get; } + private readonly List databases; + private readonly XAiApiDatabaseDescriptor apiDatabaseDescriptor; + #endregion + + // + #region Constructor(s) ... + public Startup(IConfiguration configuration) + { + // + Configuration = configuration; + + // + // Create an Api Descriptor Class Instance ... + apiDatabaseDescriptor = new XAiApiDatabaseDescriptor(); + databases = new List + { + apiDatabaseDescriptor + }; + } + #endregion + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register HttpService ... + services.AddXHttpService(Configuration); + + // + // Add Api V1 Versioning ... + services.AddXApiV1Versioning(); + + // + // OAuthIntrospectin Http Client Handler ... + // this is for handling SSLErrors ... + services.AddHttpClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true + }; + }); + + // + // Register Authorizations ... + services.AddXAuthorization(); + + // + services.AddControllers() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + x.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + + // + var lifeTime = ServiceLifetime.Scoped; + var repositoryType = xDataService.Constants.XRepositoryType.EF; + + // + // Register XIdentity Service ... + services.AddXIdentityService( + lifeTime: lifeTime, + configuration: Configuration + ); + + // + // Storage Service ... + services.AddXStorageService(Configuration); + + // + // Push Service ... + services.AddXPushService( + lifeTime: lifeTime, + config: Configuration + ); + + // + // Preparing DataBases ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + // Register DataBase ... + services.AddXDatabase( + lifeTime: lifeTime, + descriptor: database, + configuration: Configuration + ); + }); + } + + // + // Register String Service ... + services.AddXStringService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register Tag Service ... + services.AddXTagService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register File Service ... + services.AddXFileService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register AiApi Data Service ... + services.AddXAiDataService( + lifeTime: lifeTime, + repositoryType: repositoryType + ); + + // + // Register XAiApi Configuration ... + services.AddXAiApiConfiguration(Configuration); + + // + // Register File Content Extractors ... + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // + services.AddSingleton(); + // services.AddSingleton(sp => + // { + // // + // var extractors = sp.GetServices(); + // return new XFileContentExtractor(extractors); + // }); + // services.AddSingleton(sp => + // sp.GetRequiredService()); + + // + // Register Default Ai Service ... + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // + // Configure the HTTP request pipeline. + var isDevelopmentEnvironment = false; + if (env.IsDevelopment()) + { + isDevelopmentEnvironment = true; + app.UseDeveloperExceptionPage(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + // Force Https Redirection ... + app.UseHttpsRedirection(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Using Routing ... + app.UseRouting(); + + // + // Use xIdentityService ... + app.UseXIdentityService(); + + // + // Use Authorization ... + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + // + // Use Storage Service ... + app.UseXStorageService(); + + // + // Use PushService ... + var pushHelper = new XPushServiceHelper(); + app.UseXPushService(pushHelper); + + // + // Using DataBase ... + if (databases.HasChild()) + { + // + databases + .ToList() + .ForEach(database => + { + // + app.UseXDatabase( + descriptor: database, + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + }); + } + + // + // Using String Service ... + app.UseXStringService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using XTag Service ... + app.UseXTagService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + + // + // Using File Service ... + app.UseXFileService( + isDevelopmentEnvironment: isDevelopmentEnvironment + ); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Configurations\XAiApiConfiguration.cs + +using xAiModels.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + + /// + /// Allowed Prompts to Use ... + /// + public XAiPromptDescriptor[] Prompts { get; set; } = []; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\ConfigurationNodeNames.cs + +namespace xAiApi.Constants +{ + public partial struct ConfigurationNodeNames + { + public const string AI_API_NODE = "AiApiConfiguration"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Constants\XAiApiConstants.cs + +namespace xAiApi.Constants +{ + public struct XAiApiConstants + { + // + // Database Descriptor ... + public const string XAiApiDbIdentifier = "xAiApiDb"; + public const string XAiApiMigrationsAssemblyIdentifier = "xAiApi"; + + // + public const string XDILogTag = "XAiApi"; + + // + public const string XAiApiIntroductionPromptName = "Introduction"; + public const string XAiApiContentExtractionPromptName = "ContentExtraction"; + + // + public const string XAiDefaultModelName = "Gemma"; + public const string XAiEmbeddingModelName = "Embed"; + public const string XAiDefaultVisionModelName = "Qwen"; + public const string XAiDefaultThinkingModelName = "Qwen"; + public const string XAiEmbeddingPromptName = "Embedding"; + + // + public const string XOpenAINoKey = "no-key-required"; + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultAiController : XAiServiceControllerBase + { + public DefaultAiController( + ILogger logger, + IXDefaultAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultEmbeddingController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public class DefaultEmbeddingController : XAiEmbeddingServiceControllerBase + { + public DefaultEmbeddingController( + ILogger logger, + IXDefaultEmbeddingService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\DefaultThinkingAiController.cs + +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Configurations; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; + +namespace xAiApi.Controllers +{ + public class DefaultThinkingAiController : XAiServiceControllerBase + { + public DefaultThinkingAiController( + ILogger logger, + IXDefaultThinkingAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\StartupController.cs + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xAiApi.Controllers +{ + /// + /// Runing when application start ... + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiEmbeddingServiceControllerBase.cs + +using System; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Providers; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiEmbeddingServiceControllerBase : XAiServiceControllerBase, IXAiEmbeddingServiceControllerBase + where TAiService : IXAiEmbeddingServiceBase + { + private readonly TAiService aiService; + + protected XAiEmbeddingServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + aiService, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embedding")] + public async Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(content); + + // + var result = await aiService.Embedding( + content: content, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("Embeddings")] + public async Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotZeroChilds(batch); + + // + var result = await aiService.Embeddings( + batch: batch, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Controllers\XAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Text.Json; +using System.Threading; +using xAiModels.Models; +using xAiApi.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiModels.Models.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xCommons.Configurations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using xIdentityService.Interfaces; +using Microsoft.Extensions.Logging; +using xIdentityService.Controllers; +using Microsoft.AspNetCore.Authorization; +using System.Linq; + +namespace xAiApi.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class XAiServiceControllerBase : XBaseIdentityApiV1Controller, IXAiServiceControllerBase + where TAiService : IXAiServiceBase + { + private readonly TAiService aiService; + + protected XAiServiceControllerBase( + ILogger logger, + TAiService aiService, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider + ) + { + this.aiService = aiService; + } + + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskText")] + public virtual async Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + var result = await aiService.AskTextAsync( + prompt: question, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("AskTextStream")] + public virtual async Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + ValidationProvider.NotEmpty(question); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // Stream Response ... + var jsonOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // + var enumerable = aiService.AskTextAsEnumerable( + prompt: question, + cancellationToken: cancellationToken + ); + + // + await foreach (var chunk in enumerable) + { + // + // Skip empty chunks ... + if (string.IsNullOrEmpty(chunk)) + { + continue; + } + + // + // Build SSE message ... + // Format: data: \n\n + // var payload = JsonSerializer.Serialize( + // new { text = chunk }, + // jsonOptions + // ); + var payload = chunk; + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + public async Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadProjects( + ownerId: userInfo.UserId, + connectionId: connectionId, + includeDefaults: includeDefaults, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + public async Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.LoadConversations( + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + public async Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await aiService.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + public async Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryProjects( + query: query, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + public async Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryConversations( + query: query, + projectId: projectId, + ownerId: userInfo.UserId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + public async Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.QueryMessages( + query: query, + ownerId: userInfo.UserId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + public async Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateProject( + item: item, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + public async Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (projectId.IsDefaultGuid() || + item.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Update Required Params ... + item.OwnerId = userInfo.UserId; + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await aiService.CreateConversation( + item: item, + projectId: projectId, + ownerId: userInfo.UserId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Content Extraction ... + /// + /// Extract Files Content ... + /// + /// + /// + /// + /// + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + public async Task> ExtractContent( + [FromForm] IFormFileCollection files, + [FromForm] string extractors = null, + CancellationToken cancellationToken = default + ) { + // + try + { + // + // Validate ... + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + string[] extractorArr = null; + if (!extractors.IsNullOrEmpty()) + { + extractorArr = [.. extractors.ParseListString()]; + } + + // + var result = await aiService.ExtractContent( + files: files, + extractors: extractorArr, + cancellationToken: cancellationToken + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + public async Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var result = await aiService.AskAsync( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + public async Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + // Configure SSE Response Headers ... + Response.Headers["X-Accel-Buffering"] = "no"; + Response.Headers["Connection"] = "keep-alive"; + Response.Headers["Content-Type"] = "text/event-stream"; + Response.Headers["Cache-Control"] = "no-cache, no-store"; + + // + // Disable response buffering ... + var responseBodyFeature = HttpContext.Features + .Get(); + responseBodyFeature?.DisableBuffering(); + + // + // TODO: Reading Fiels Form Collection and Attach it ... + var enumerable = aiService.AskAsEnumerable( + files: null, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + await foreach (var resp in enumerable) + { + // + // Skip Empty Responses ... + if (resp.IsNullOrDefault()) + { + continue; + } + + // + var payload = resp.ToJSON(camelCase: true); + + // + // Write SSE Event ... + await Response.WriteAsync($"{payload}", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + + // + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + await Response.Body.FlushAsync(); + } + } + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + public async Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ) + { + // + try + { + // + // Validate ... + if (!request.IsValid()) + { + XException.InvalidArgs.Throw(); + } + + // + var userInfo = await GetUserInfo(); + var connectionId = GetConnectionId(); + + // + var result = await aiService.AskAsync( + files: files, + userInfo: userInfo, + prompt: request.Prompt, + ownerId: userInfo.UserId, + connectionId: connectionId, + projectId: request.ProjectId, + cancellationToken: cancellationToken, + conversationId: request.ConversationId + ); + + // + return Ok(result); + } + catch (Exception ex) + { + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDatabaseDescriptor.cs + +using xAiApi.Constants; +using xDataService.Interfaces; +using xDataService.Models; + +namespace xAiApi.Database +{ + /// + /// Ai Api Database Descriptor ... + /// + public class XAiApiDatabaseDescriptor : XDatabaseDescriptor, IXDatabaseDescriptor + { + public XAiApiDatabaseDescriptor() : base( + name: XAiApiConstants.XAiApiDbIdentifier, + repositories: [] + ) + { + OptionsBuilder = b => b.MigrationsAssembly(XAiApiConstants.XAiApiMigrationsAssemblyIdentifier); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Database\XAiApiDbContext.cs + +using Microsoft.EntityFrameworkCore; +using xAiApi.Constants; +using xAiModels.Providers; +using xDataService.Configuration; +using xDataService.Db; +using xDataService.Providers; +using xFileService.Providers; +using xStringService.Providers; +using xTagService.Providers; + +namespace xAiApi.Database +{ + /// + /// Db Context of AiApi ... + /// + public class XAiApiDbContext : XDbContext + { + // + #region DbSets ... + #endregion + + // + #region Navigation Property Entities ... + #endregion + + // + #region Constructor ... + public XAiApiDbContext( + DbContextOptions options, + XDataServiceConfiguration config, + XEntityRegisterarHandlerService dynamicEntityHandlers + ) : base( + config: config, + options: options, + database: XAiApiConstants.XAiApiDbIdentifier, + dynamicEntityHandlers: dynamicEntityHandlers, + dynamicEntityRegisterarsNames: [ + nameof(XTagEntityRegisterar), + nameof(XFileEntityRegisterar), + nameof(XStringEntityRegisterar), + nameof(XAiMessageEntityRegisterar), + nameof(XAiProjectEntityRegisterar), + nameof(XAiConversationEntityRegisterar), + ] + ) + { } + #endregion + + // + #region Overrides ... + // + public override void OnXModelCreating(ModelBuilder modelBuilder) + { } + + // + public override void OnXConfiguring(DbContextOptionsBuilder optionsBuilder) + { } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\DI\XDIHelperExtension.cs + +using System; +using xAiApi.Constants; +using xAiApi.Extensions; +using xCommons.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace xAiApi.DI +{ + public static partial class XDIHelperExtension + { + /// + /// Extract AI Configuration ... + /// + /// + /// + public static XAiApiConfiguration GetXAiApiConfiguration(this IConfiguration source) + { + // + XAiApiConfiguration result = null; + + // + var section = source.GetSection(ConfigurationNodeNames.AI_API_NODE); + if (!section.IsNull()) + { + result = section.Get(); + } + + // + return result; + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + IConfiguration configuration + ) + { + // + var apiConfig = configuration.GetXAiApiConfiguration(); + source.AddXAiApiConfiguration(apiConfig); + } + + /// + /// Register AiApi Configuration ... + /// + /// + /// + public static void AddXAiApiConfiguration( + this IServiceCollection source, + XAiApiConfiguration configuration + ) + { + // + if (!configuration.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + source.AddSingleton(configuration); + } + + // + #region Private ... + /// + /// a LogTag for Service ... + /// + private static string XLogTag = XAiApiConstants.XDILogTag; + + /// + /// print a log in Console ... + /// + /// + private static void Log(string message) + { + Console.WriteLine($"{XLogTag} => {message}"); + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XModelsExtensions.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Net.Http; +using xAiApi.Constants; +using xAiModels.Models; +using System.Threading; +using System.ClientModel; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using System.ClientModel.Primitives; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()) && + source.Prompts.All(m => m.IsValid()) && + source.Prompts.Any(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + + /// + /// Retrieve Introduction Prompt ... + /// + /// + /// + public static XAiPromptDescriptor GetIntroductionPrompt(this XAiApiConfiguration source) + { + // + XAiPromptDescriptor result = null; + + // + if (source.IsValid()) + { + // + result = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + XAiApiConstants.XAiApiIntroductionPromptName, + System.StringComparison.InvariantCultureIgnoreCase)); + } + + // + return result; + } + + /// + /// Select Specified Prompts and Parse it's Template + /// and Generate Message ... + /// + /// + /// + /// + /// + public static string GetPrompt( + this XAiApiConfiguration source, + string name = null, + IDictionary @params = null + ) + { + // + var result = string.Empty; + + // + // Normalize ... + if (name.IsNullOrEmpty()) + { + name = XAiApiConstants.XAiApiIntroductionPromptName; + } + + // + if (source.IsValid()) + { + // + var prompt = source.Prompts + .FirstOrDefault(p => p.Name + .Equals( + name, + System.StringComparison.InvariantCultureIgnoreCase + ) + ); + if (!prompt.IsNullOrDefault()) + { + // + result = prompt.Template; + + // + // Handle Params ... + if (!@params.IsNullOrDefault() && @params.HasChild()) + { + // + // Select Params ... + var existsParams = prompt.Params + .Where(p => @params.Any(pr => pr.Key == p.Name)) + .Distinct() + .ToList(); + + // + // Replace Selected Params ... + existsParams + .ForEach(pk => + { + result = result.Replace(pk.Param, @params[pk.Name]); + }); + } + } + } + + // + return result; + } + + /// + /// Provide HttpClient for Common Usages ... + /// + /// + /// + public static HttpClient GetHttpClient(this string source) + { + // + var result = new HttpClient + { + // + // Disable timeout completely (not recommended for production) + Timeout = Timeout.InfiniteTimeSpan, + + // + BaseAddress = source.IsNullOrEmpty() + ? null + : new Uri(source), + + // + // OR set a very long timeout (e.g., 30 minutes for large file uploads) + // Timeout = TimeSpan.FromMinutes(30) + }; + + // + return result; + } + + /// + /// Instance and Retrieve Chat Client ... + /// + /// + /// + public static IChatClient GetClient(this XAiModelDescriptor source) + { + // + // Try to Initialize LLm ... + var model = source.LLM; + var apiKey = source.ApiKey; + var url = new Uri(source.Url); + var httpClient = source.Url.GetHttpClient(); + + // + IChatClient result = null; + switch (source.Provider) + { + // + case XAiModelProviderType.Ollama: + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.OpenAI: + // + // Instance Ollama Client using Custom HttpCLient ... + var openAiClient = new OpenAIClient( + new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), + new OpenAIClientOptions + { + Endpoint = url, + Transport = new HttpClientPipelineTransport(httpClient) + } + ); + result = + new ChatClientBuilder( + openAiClient + .GetChatClient(model) + .AsIChatClient()) + .UseFunctionInvocation() + .Build(); + break; + + // + case XAiModelProviderType.DeepSeek: + break; + + // + case XAiModelProviderType.HuggingFace: + break; + + // + default: + break; + } + + // + if (result.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Extensions\XProgramExtensions.cs + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Authorization; +using xCommons.Extensions; +using xIdentityHelper; + +namespace xAiApi.Extensions +{ + public static class XProgramExtensions + { + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OllamaSharp; + +namespace xAiApi.Interfaces +{ + /// + /// Base Embedding Provider Service ... + /// + public interface IXAiEmbeddingServiceBase : IXAiServiceBase + { + /// + /// Embedding Specified String ... + /// + /// + /// + /// + Task Embedding( + string content, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ); + + /// + /// Embedding Generator ... + /// + /// + IEmbeddingGenerator> GetEmbeddingGenerator(); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiEmbeddingServiceControllerBase.cs + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace xAiApi.Interfaces +{ + public interface IXAiEmbeddingServiceControllerBase : IXAiServiceControllerBase + { + /// + /// Request For Embedding Specified String ... + /// + /// + /// + /// + [HttpGet("Embedding")] + Task> Embedding( + [FromQuery] string content, + CancellationToken cancellationToken = default + ); + + /// + /// Request For Embedding Specified Batch ... + /// + /// + /// + /// + [HttpGet("Embeddings")] + Task>> Embeddings( + [FromQuery] string[] batch, + CancellationToken cancellationToken = default + ); + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceBase.cs + +using System; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using xIdentityModels.Models; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + /// + /// Describe a Base Service Actions of any Ai Based Services ... + /// + public interface IXAiServiceBase : IDisposable + { + /// + /// Chat Options ... + /// + ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + XAiModelDescriptor Descriptor { get; } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + IAsyncEnumerable AskTextAsEnumerable( + string prompt, + CancellationToken cancellationToken = default + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + string prompt = null, + bool forcePrompt = true + ); + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ); + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + /// + Task ExtractContent( + IFormFileCollection files, + string[] extractors = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXAiServiceControllerBase.cs + +using System; +using xModels.Dtos; +using System.Threading; +using xAiModels.Models; +using xAiModels.Models.Dtos; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace xAiApi.Interfaces +{ + public interface IXAiServiceControllerBase + { + // + #region Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpGet("AskText")] + Task> AskText( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpGet("AskTextStream")] + Task AskTextStream( + [FromQuery] string question, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Loaders ... + /// + /// Retrieve AI Projects ... + /// + /// + /// + /// + [HttpGet("Projects")] + Task>> LoadProjects( + [FromQuery] bool includeDefaults = false, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Project's Conversations ... + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations")] + Task>> LoadConversations( + [FromRoute] Guid projectId, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve Specified AI Conversation's Messages ... + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages")] + Task>> LoadMessages( + [FromRoute] Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + /// + [HttpGet("Projects/Query")] + Task>> QueryProjects( + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + [HttpGet("Projects/{projectId}/Conversations/Query")] + Task>> QueryConversations( + [FromRoute] Guid projectId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + [HttpGet("Conversations/{conversationId}/Messages/Query")] + Task>> QueryMessages( + [FromRoute] Guid conversationId, + [FromQuery] XQuery query, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + [HttpPost("Projects")] + Task> CreateProject( + [FromBody] XAiProjectDto item, + CancellationToken cancellationToken = default + ); + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + [HttpPost("Projects/{projectId}/Conversations")] + Task> CreateConversation( + [FromRoute] Guid projectId, + [FromBody] XAiConversationDto item, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Content Extraction ... + /// + /// Extract Files Content ... + /// + /// + /// + /// + /// + [HttpPost("ExtractContent")] + [Consumes("multipart/form-data")] + Task> ExtractContent( + [FromForm] IFormFileCollection files, + [FromForm] string extractors = null, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Data Actions ... + /// + /// Ask Question and Get Response ... + /// + /// + /// + /// + [HttpPost("Ask")] + Task> Ask( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question and Get Response Stream ... + /// + /// + /// + /// + [HttpPost("AskStream")] + Task AskStream( + [FromBody] XAiResponseRequest request, + CancellationToken cancellationToken = default + ); + + /// + /// Ask Question with Files and Get Response ... + /// + /// + /// + /// + [HttpPost("AskWithFiles")] + [Consumes("multipart/form-data")] + Task> AskWithFiles( + [FromForm] XAiResponseRequest request, + [FromForm] IFormFileCollection files, + CancellationToken cancellationToken = default + ); + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultAiService : IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultEmbeddingService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultEmbeddingService : IXAiEmbeddingServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\IXDefaultThinkingAiService.cs + +namespace xAiApi.Interfaces +{ + public interface IXDefaultThinkingAiService: IXAiServiceBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXAudioFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXAudioFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXDocxFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXDocxFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXExcelFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXExcelFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXFileContentExtractor.cs + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models; + +namespace xAiApi.Interfaces.Extractors +{ + /// + /// a Base Service for Extracting File Content ... + /// + public interface IXFileContentExtractorBase + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + bool CanExtract(string mimeType); + + /// + /// Extract text content from file stream ... + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ); + } + + /// + /// a Service for Extracting File Content ... + /// + public interface IXFileContentExtractor + { + /// + /// Check if this extractor supports the specified MIME type ... + /// + /// + /// + /// + bool CanExtract( + string mimeType, + string[] extractors = null + ); + + /// + /// Extract text content from file stream ... + /// + /// + /// + /// + /// + /// + Task ExtractAsync( + Stream fileStream, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ); + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + /// + Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ); + + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXImageFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXImageFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPdfFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPdfFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXPlainTextFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXPlainTextFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Interfaces\Extractors\IXVisionFileContentExtractor.cs + +namespace xAiApi.Interfaces.Extractors +{ + public interface IXVisionFileContentExtractor : IXFileContentExtractorBase + { } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003121152_InitialMigrationAiApi.cs + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace xAiApi.Migrations +{ + public partial class InitialMigrationAiApi : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AiConversations", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ProjectId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiMessages", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Content = table.Column(nullable: false), + Role = table.Column(nullable: false), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false), + ConversationId = table.Column(maxLength: 255, nullable: false), + MetaDatas = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AiMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AiProjects", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Title = table.Column(maxLength: 255, nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Prompt = table.Column(nullable: true), + CreatedOn = table.Column(nullable: false), + UpdatedAt = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AiProjects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(nullable: false), + Deleted = table.Column(nullable: false), + Type = table.Column(nullable: false), + OwnerId = table.Column(maxLength: 255, nullable: false), + Name = table.Column(maxLength: 255, nullable: false), + Path = table.Column(nullable: false), + Thumb = table.Column(nullable: true), + ThumbPath = table.Column(nullable: true), + UploadedOn = table.Column(nullable: false), + FileName = table.Column(nullable: true), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Strings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Language = table.Column(maxLength: 10, nullable: false), + ResourceTitle = table.Column(maxLength: 255, nullable: false), + TranslatedValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Strings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Deleted = table.Column(nullable: false), + Tag = table.Column(maxLength: 255, nullable: false), + References = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AiConversations"); + + migrationBuilder.DropTable( + name: "AiMessages"); + + migrationBuilder.DropTable( + name: "AiProjects"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "Strings"); + + migrationBuilder.DropTable( + name: "Tags"); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\20261003121152_InitialMigrationAiApi.Designer.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + [Migration("20261003121152_InitialMigrationAiApi")] + partial class InitialMigrationAiApi + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Migrations\XAiApiDbContextModelSnapshot.cs + +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using xAiApi.Database; + +namespace xAiApi.Migrations +{ + [DbContext(typeof(XAiApiDbContext))] + partial class XAiApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiConversations"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("MetaDatas") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiMessages"); + }); + + modelBuilder.Entity("xAiModels.Models.Entities.XAiProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasMaxLength(1000); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Prompt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AiProjects"); + }); + + modelBuilder.Entity("xFileService.Models.Entities.XFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Thumb") + .HasColumnType("TEXT"); + + b.Property("ThumbPath") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UploadedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("xStringService.Models.Entities.XString", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Language") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(10); + + b.Property("ResourceTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("TranslatedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Strings"); + }); + + modelBuilder.Entity("xTagService.Models.Entities.XTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("References") + .HasColumnType("TEXT"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAiEmbeddingServiceBase.cs + +using System; +using OpenAI; +using System.Linq; +using OllamaSharp; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xAiService.Helpers; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Extensions; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; + +namespace xAiApi.Providers +{ + /// + /// Base Embedding Provider Service ... + /// + public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase + { + private readonly IXFileProvider fileProvider; + private readonly XAiModelDescriptor descriptor; + private readonly ILogger logger; + + protected XAiEmbeddingServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor, + string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + null + ) + { + // + // Prepare Embedding Model Descriptor ... + descriptor = configuration.GetModel(embeddingModelName); + if (!descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + this.fileProvider = fileProvider; + + // + this.logger = logger; + } + + /// + /// Embedding Specified String ... + /// + /// + /// + /// + public virtual async Task Embedding( + string content, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (content.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [content], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + // Generae Result ... + var result = response + .First()? + .Vector + .ToArray() + ?? []; + if (result.Length == 0) + { + XException.ActionFailed.Throw(); + } + + // + // Normalize ... + VectorHelper.Normalize(result); + + // + return result; + } + + /// + /// Embedding Specified Batch ... + /// + /// + /// + /// + public virtual async Task> Embeddings( + string[] batch, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!batch.HasChild() || + !batch.All(x => !x.IsNullOrEmpty()) + ) + { + XException.InvalidArgs.Throw(); + } + + // + using var generator = GetEmbeddingGenerator(); + + // + GeneratedEmbeddings> response = null; + response = await generator.GenerateAsync( + values: [.. batch], + cancellationToken: cancellationToken + ); + + // + if (response.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + var result = response + .Select(e => e.Vector.ToArray()) + .ToList(); + + // + // Normalize ... + foreach (var vector in result) + { + VectorHelper.Normalize(vector); + } + + // + return result; + } + + /// + /// Embedding Generator ... + /// + /// + public IEmbeddingGenerator> GetEmbeddingGenerator() + { + // + IEmbeddingGenerator> result = null; + + // + if (!descriptor.IsValid()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + switch (descriptor.Provider) + { + // + case XAiModelProviderType.OpenAI: + // + var openAiClient = new OpenAIClient( + new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey), + new OpenAIClientOptions + { + Endpoint = new Uri(descriptor.Url), + Transport = new HttpClientPipelineTransport(descriptor.Url.GetHttpClient()) + } + ); + result = openAiClient + .GetEmbeddingClient(descriptor.LLM) + .AsIEmbeddingGenerator(); + break; + + // + case XAiModelProviderType.Ollama: + // + var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url)); + result = ollamaClient; + break; + } + + // + if (result.IsNull()) + { + XException.InvalidData.Throw("Invalid Embedding Model ..."); + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XAIServiceBase.cs + +using System; +using OpenAI; +using OllamaSharp; +using System.Linq; +using xModels.Dtos; +using System.Net.Http; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Interfaces; +using xAiApi.Extensions; +using System.ClientModel; +using xCommons.Providers; +using xCommons.Extensions; +using xAiModels.Constants; +using xAiModels.Interfaces; +using xAiModels.Extensions; +using xAiService.Constants; +using xAiApi.Configurations; +using xExceptions.Constants; +using xAiModels.Models.Dtos; +using xAiService.Extensions; +using System.Threading.Tasks; +using xIdentityModels.Models; +using xFileService.Interfaces; +using Microsoft.Extensions.AI; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using System.ClientModel.Primitives; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers +{ + /// + /// Base Ai Service ... + /// + public abstract class XAIServiceBase : IXAiServiceBase + { + /// + /// Chat Options ... + /// + public ChatOptions Options { get; } + + /// + /// Descriptor of Models which used in Service ... + /// + public XAiModelDescriptor Descriptor { get; } + + private readonly IXFileProvider fileProvider; + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + private readonly XValidationProvider validationProvider; + private readonly IXFileContentExtractor fileContentExtractor; + + protected XAIServiceBase( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + string model = null + ) + { + // + this.logger = logger; + this.fileProvider = fileProvider; + this.dataProvider = dataProvider; + this.configuration = configuration; + this.validationProvider = validationProvider; + this.fileContentExtractor = fileContentExtractor; + + // + // Prepare Embedding Model Descriptor ... + Descriptor = configuration.GetModel(model); + if (!Descriptor.IsValid()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Prepare Chat Options based On Tools ... + Options = new ChatOptions(); + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskTextAsync( + string prompt, + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = Descriptor.GetClient(); + var history = GetHistory(prompt); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskTextAsEnumerable( + string prompt, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Check Prompt Exists ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = Descriptor.GetClient(); + var history = GetHistory(prompt); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + string prompt = null, + bool forcePrompt = true + ) + { + // + // Validate Args ... + if (prompt.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt() + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forcePrompt && + prompt is not null && + !string.IsNullOrWhiteSpace(prompt)) + { + result.Add(promptMessage); + } + + // + return result; + } + + /// + /// Prepare a Message History List for LLM Communication based on Prompt ... + /// + /// + /// + /// + public virtual IList GetHistory( + ChatMessage message = null, + bool forceMessage = true + ) + { + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + !message.IsNullOrDefault()) + { + result.Add(message); + } + + // + return result; + } + #endregion + + // + #region Loaders ... + /// + /// Load AI Projects ... + /// + /// + /// + /// + /// + /// + public virtual async Task> LoadProjects( + string ownerId, + string connectionId = null, + bool includeDefaults = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadProjects( + ownerId: ownerId, + fillConversations: false, + connectionId: connectionId, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handle Defaults Including ... + if (includeDefaults) + { + // + // Here we have to fill Conversations of Default Project ... + var defaultProjectId = result.GetDafualtProject(ownerId); + var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); + result = result.Where(x => x.Id != defaultProjectId); + + // + // Reading Conversations of Specified Default Project ... + var defaultProjectConversations = await dataProvider.LoadConversations( + fillMessages: false, + connectionId: connectionId, + projectId: defaultProjectId, + cancellationToken: cancellationToken + ); + var defaultConversationId = defaultProjectConversations.GetDefaultConversation( + ownerId: ownerId, + projectId: defaultProjectId + ); + var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); + defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); + + // + // Reading Default Conversations Messages ... + var defaultConversationMessages = await dataProvider.LoadMessages( + cancellationToken: cancellationToken, + conversationId: defaultConversationId + ); + + // + defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; + defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; + defaultProject.Conversations = [.. defaultProjectConversations]; + result = [defaultProject, .. result]; + } + + // + return result; + } + + /// + /// Load Conversations of Specified AI Project ... + /// + /// + /// + /// + /// + public virtual async Task> LoadConversations( + Guid projectId, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadConversations( + fillMessages: false, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public virtual async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Queries ... + /// + /// Query Projects ... + /// + /// + /// + /// + /// + public virtual async Task> QueryProjects( + string ownerId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = await dataProvider.QueryProjects( + query: query, + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Conversations ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryConversations( + string ownerId, + Guid projectId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Project ... + var project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != project.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryConversations( + query: query, + ownerId: ownerId, + projectId: projectId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Query Messages ... + /// + /// + /// + /// + /// + /// + public virtual async Task> QueryMessages( + string ownerId, + Guid conversationId, + XQuery query, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (ownerId.IsNullOrEmpty() || + conversationId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation ... + var conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (ownerId != conversation.OwnerId) + { + XException.NotAllowed.Throw(); + } + + // + var result = await dataProvider.QueryMessages( + query: query, + ownerId: ownerId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Creators ... + /// + /// Create an AI Project ... + /// + /// + /// + /// + /// + /// + public virtual async Task CreateProject( + string ownerId, + XAiProjectDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateProject( + item: item, + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Create an AI Conversation ... + /// + /// + /// + /// + /// + /// + /// + public virtual async Task CreateConversation( + string ownerId, + Guid projectId, + XAiConversationDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + var result = await dataProvider.CreateConversation( + item: item, + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + return result; + } + #endregion + + // + #region Content Extraction ... + /// + /// Try to Extract Files Content ... + /// + /// + /// + /// + /// + public async Task ExtractContent( + IFormFileCollection files, + string[] extractors = null, + CancellationToken cancellationToken = default + ) + { + // + if (!files.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + var extractionResults = new List(); + foreach (var file in files) + { + // + if (file.IsNull() || file.Length == 0) + { + continue; + } + + // + var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; + if (!fileContentExtractor.CanExtract(mimeType, extractors)) + { + continue; + } + + // + try + { + // + using var stream = file.OpenReadStream(); + + // + var fr = await fileContentExtractor.ExtractRichAsync( + fileStream: stream, + mimeType: mimeType, + extractors: extractors, + fileName: file.FileName, + cancellationToken: cancellationToken + ); + + // + if (fr.IsValid()) + { + extractionResults.Add(fr); + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + continue; + } + } + + // + var result = string.Empty; + if (extractionResults.HasChild()) + { + // + extractionResults.ForEach(er => + { + result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; + }); + } + + // + return result; + } + #endregion + + // + #region Data Actions ... + /// + /// Ask a Question as Prompt and Retrieve Response ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async Task AskAsync( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Upload Files ... + IEnumerable uploadedFiles = null; + if (files.HasChild()) + { + // + // Validate User Info ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Upload Files ... + uploadedFiles = await fileProvider.Upload( + files: files, + userInfo: userInfo, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + + // + // Attached Uploaded File Id's as Meta Data ... + if (uploadedFiles.HasChild()) + { + // + var fileIds = uploadedFiles.Select(f => f.Id).ToList(); + promptMessage.MetaDatas = new Dictionary + { + { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } + }.ToJSON(); + } + + // + // Save Prompt Message ... + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... + IList fileContents = []; + if (uploadedFiles.HasChild()) + { + // + foreach (var uf in uploadedFiles) + { + // + try + { + // + // Add Reference File to Prompt Message ... + await fileProvider.AddReference( + id: uf.Id, + userInfo: userInfo, + connectionId: connectionId, + forProvidedId: promptMessage.Id, + cancellationToken: cancellationToken, + providedFor: XAiServiceConstants.XAiMessageFilesIdentifier + ); + + // + // Handle File Content Extraction and Attach to Prompt Message ... + var fileDescriptor = await fileProvider.GetFileDescriptor( + id: uf.Id, + cancellationToken: cancellationToken + ); + if (fileDescriptor == null || fileDescriptor.Stream == null) + { + continue; + } + + // + var extractionResult = await fileContentExtractor.ExtractRichAsync( + fileStream: fileDescriptor.Stream, + fileName: uf.FileName ?? uf.Name, + mimeType: fileDescriptor.MIMEType, + cancellationToken: cancellationToken + ); + if (extractionResult.IsValid()) + { + // + var contents = extractionResult.ToAIContents(); + fileContents = [.. fileContents, .. contents]; + + // + // ذخیره Metadata برای ردیابی + logger.LogInformation( + "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", + uf.FileName, + !string.IsNullOrEmpty(extractionResult.Text), + extractionResult.Images.Count, + extractionResult.UsedImageFallback + ); + } + + // + fileDescriptor.Stream.Dispose(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); + } + } + } + + // + // Converts to ChatMessage ... + var promptChatMessage = promptMessage + .ToChatMessages(fileContents); + + // + // Ask Questions From LLM ... + var answer = await AskLLMAsync( + history: history, + prompt: promptChatMessage, + cancellationToken: cancellationToken + ); + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + CreatedOn = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = conversationId, + ConversationTitle = conversation.Title + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + string prompt, + string ownerId, + Guid projectId, + Guid conversationId, + string connectionId = null, + IFormFileCollection files = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + validationProvider + .GroupValidationBuilder() + .AddNotEmpty(prompt) + .AddNotEmpty(ownerId) + .ValidateGroup(); + + // + // Check Project ... + XAiProjectDto project = null; + if (projectId.IsDefaultGuid()) + { + // + project = await dataProvider.GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + projectId = project.Id; + } + else + { + // + project = await dataProvider.GetProject( + id: projectId, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation ... + XAiConversationDto conversation = null; + if (conversationId.IsDefaultGuid()) + { + // + conversation = await dataProvider.GetDefaultConversation( + ownerId: ownerId, + projectId: projectId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + + // + conversationId = conversation.Id; + } + else + { + // + conversation = await dataProvider.GetConversation( + id: conversationId, + fillMessages: false, + cancellationToken: cancellationToken + ); + } + if (conversation.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Reading Messages ... + var messages = await dataProvider.LoadMessages( + conversationId: conversationId, + cancellationToken: cancellationToken + ); + + // + // TODO: Parse Files ... + // for Attached them into Prompt Message ... + + // + // Handle Memory ... + IList history = await PrepareMemory( + project: project, + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Prepare and Add Prompt Message ... + var promptMessage = new XAiMessageDto + { + Content = prompt, + OwnerId = ownerId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId, + }; + promptMessage = await dataProvider.AddMessage( + item: promptMessage, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + promptMessage = await dataProvider.Enrich( + source: promptMessage, + cancellationToken: cancellationToken + ); + + // + // Attach Uploaded Files ... + var promptChatMessage = promptMessage + .ToChatMessages(); + history.Add(promptChatMessage); + + // + // Ask Questions From LLM ... + using var client = Descriptor.GetClient(); + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: history + ); + + // + long sequence = 1; + var answer = string.Empty; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var resp = new XAiResonseMessage + { + OwnerId = ownerId, + Content = res.Text, + Sequence = sequence, + Role = XAiChatRole.System, + RespondTo = promptMessage.Id, + RespondedOn = DateTime.UtcNow, + }; + + // + sequence++; + answer += res.Text; + + // + yield return resp; + } + + // + // Prepare Answer AI Message ... + var result = new XAiMessageDto + { + Content = answer, + OwnerId = ownerId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + ConversationId = conversationId + }; + result = await dataProvider.AddMessage( + item: result, + connectionId: connectionId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + + // + #region Private ... + /// + /// Implementing Memory Strategy here ... + /// + /// + /// + /// + /// + private async Task> PrepareMemory( + XAiProjectDto project, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + { + // + var result = new List(); + + // + // Retrieve Introduction Prompt ... + var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); + if (introductionPrompt.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + var introductionMessage = new ChatMessage( + role: ChatRole.System, + content: introductionPrompt + ); + + // + result.Add(introductionMessage); + + // + if (!project.IsNullOrDefault() && + !project.Prompt.IsNullOrEmpty()) + { + // + // TODO: Handle Project Files Here ... + var projectMessage = new ChatMessage( + role: ChatRole.System, + contents: [new TextContent(project.Prompt)] + ); + result.Add(projectMessage); + } + + // + if (messages.HasChild()) + { + // + // TODO: Here we can Implement Memory Management based on + // Last nth of Messages, or Summarization of Messages, etc ... + result = [..result, ..messages + .OrderBy(m => m.CreatedOn) + .Select(m => m.ToChatMessages())]; + } + + // + return result; + } + + /// + /// Ask Quetion from LLM ... + /// + /// + /// + /// + /// + private async Task AskLLMAsync( + ChatMessage prompt, + IList history, + CancellationToken cancellationToken = default + ) + { + // + if (prompt.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + history.Add(prompt); + using var client = Descriptor.GetClient(); + var response = await client.GetResponseAsync( + messages: history, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + #endregion + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultAiService : XAIServiceBase, IXDefaultAiService + { + public XDefaultAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultEmbeddingService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; +using xCommons.Providers; + +namespace xAiApi.Providers +{ + public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService + { + public XDefaultEmbeddingService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + ILogger logger, + IXFileContentExtractor fileContentExtractor + ) : base( + fileProvider, + dataProvider, + configuration, + validationProvider, + logger, + fileContentExtractor, + XAiApiConstants.XAiEmbeddingModelName + ) + { } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\XDefaultThinkingAiService.cs + +using xAiApi.Constants; +using xAiApi.Interfaces; +using xCommons.Providers; +using xAiModels.Interfaces; +using xAiApi.Configurations; +using Microsoft.Extensions.AI; +using xFileService.Interfaces; +using Microsoft.Extensions.Logging; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers +{ + public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService + { + public XDefaultThinkingAiService( + IXFileProvider fileProvider, + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration, + XValidationProvider validationProvider, + IXFileContentExtractor fileContentExtractor, + ReasoningEffort effort = ReasoningEffort.Medium, + ReasoningOutput output = ReasoningOutput.Full + ) : base( + fileProvider, + dataProvider, + logger, + configuration, + validationProvider, + fileContentExtractor, + XAiApiConstants.XAiDefaultThinkingModelName + ) + { + // + // Preparing Reasoning ... + Options.Reasoning = new ReasoningOptions + { + Output = output, + Effort = effort, + }; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XAudioFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using Whisper.net; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.EMMA; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from Audio files ... + /// + public class XAudioFileContentExtractor : IXAudioFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "audio/mpeg", + "audio/mp3", + "audio/wav", + "audio/wave", + "audio/ogg", + "audio/m4a", + "audio/mp4", + "audio/webm" + ]; + + private readonly string language; + private readonly string whisperModelPath; + + public XAudioFileContentExtractor() : this( + language: "fa", + whisperModelPath: "Models/ggml-base.bin" + ) + { } + + public XAudioFileContentExtractor( + string whisperModelPath = "Models/ggml-base.bin", + string language = "fa" + ) + { + // + this.language = language; + this.whisperModelPath = whisperModelPath; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "audio", + mimeType, + cancellationToken + ); + + // + return result.AudioTranscript; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + using var wavStream = await ConvertToWavAsync( + memoryStream, + cancellationToken + ); + + // + using var factory = WhisperFactory.FromPath(whisperModelPath); + using var processor = factory.CreateBuilder() + .WithLanguage(language) + .Build(); + + // + var segments = new System.Text.StringBuilder(); + await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken)) + { + segments.Append(segment.Text); + } + + // + result.AudioTranscript = segments.ToString().Trim(); + result.Text = result.AudioTranscript; + } + catch (Exception ex) + { + result.ErrorMessage = $"Audio extraction failed: {ex.Message}"; + } + + // + return result; + } + + /// + /// Audio Format Converting ... + /// + private async Task ConvertToWavAsync( + Stream inputStream, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(inputStream); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XDocxFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Packaging; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts text content from DOCX files using DocumentFormat.OpenXml ... + /// + public class XDocxFileContentExtractor : IXDocxFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var wordDocument = WordprocessingDocument.Open(memoryStream, false)) + { + // + var body = wordDocument.MainDocumentPart?.Document?.Body; + if (body != null) + { + // + var paragraphs = body.Elements(); + foreach (var paragraph in paragraphs) + { + // + var text = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + stringBuilder.AppendLine(text); + } + } + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XExcelFileContentExtractor.cs + +using System; +using System.IO; +using System.Data; +using System.Linq; +using System.Text; +using ExcelDataReader; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XExcelFileContentExtractor : IXExcelFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx + "application/vnd.ms-excel" // .xls + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var stringBuilder = new StringBuilder(); + + // + using (var reader = ExcelReaderFactory.CreateReader(memoryStream)) + { + // + var result = reader.AsDataSet(); + foreach (DataTable table in result.Tables) + { + // + stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---"); + + // + var headers = new string[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + { + headers[i] = table.Columns[i].ColumnName; + } + stringBuilder.AppendLine(string.Join(" | ", headers)); + stringBuilder.AppendLine(new string('-', 50)); + + // + foreach (DataRow row in table.Rows) + { + // + var rowValues = new string[row.ItemArray.Length]; + for (int i = 0; i < row.ItemArray.Length; i++) + { + rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty; + } + stringBuilder.AppendLine(string.Join(" | ", rowValues)); + } + stringBuilder.AppendLine(); + } + } + + // + return stringBuilder.ToString(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XFileContentExtractor.cs + +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using xCommons.Extensions; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Composite extractor that delegates to appropriate extractor + /// based on MIME type ... + /// + public class XFileContentExtractor : IXFileContentExtractor + { + // + private readonly IList _extractors = []; + + public XFileContentExtractor( + IEnumerable availableExtractors + ) + { + // + _extractors = [.. availableExtractors + .Where(e => e.GetType() != typeof(XFileContentExtractor))]; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + /// + /// + /// + public bool CanExtract( + string mimeType, + string[] extractors = null + ) + { + // + return GetExtractors(extractors) + .Any(e => e.CanExtract(mimeType)); + } + + /// + /// Extract text content from file stream ... + /// + /// + /// + /// + /// + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ) + { + // + var extractor = GetExtractors(extractors) + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + string[] extractors = null, + CancellationToken cancellationToken = default + ) + { + // + var extractor = GetExtractors(extractors) + .FirstOrDefault(e => e.CanExtract(mimeType)); + if (extractor.IsNull()) + { + // + XException.NotAllowed.Throw( + $"Unsupported file type: {mimeType}" + ); + } + + // + var result = await extractor + .ExtractRichAsync( + mimeType: mimeType, + fileName: fileName, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Extract Provided Extractors ... + /// + /// + /// + private IList GetExtractors(string[] extractors = null) + { + // + var result = new List(); + + // + if (extractors.IsNull() || !extractors.HasChild()) + { + // + result = [.. _extractors]; + } + else + { + // + result = [.. _extractors + .Where(e => extractors + .Any(ex => e.GetType().FullName.Contains(ex)))]; + } + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XImageFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + public class XImageFileContentExtractor : IXImageFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/gif", + "image/webp", + "image/bmp" + ]; + + /// + /// آیا OCR فعال است (برای مدل‌های غیر Vision) ... + /// + private readonly bool _enableOcrFallback; + + public XImageFileContentExtractor() : this(enableOcrFallback: true) + { } + + public XImageFileContentExtractor(bool enableOcrFallback = true) + { + _enableOcrFallback = enableOcrFallback; + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "image", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = mimeType, + Description = $"Attached image: {fileName}" + }); + + // OCR Fallback برای مدل‌های متنی + if (_enableOcrFallback) + { + // + try + { + // + var ocrText = await PerformOcrAsync(imageBytes, cancellationToken); + if (!string.IsNullOrWhiteSpace(ocrText)) + { + result.Text = ocrText; + } + } + catch + { + // OCR failed, but image is still available for Vision + } + } + + // + return result; + } + + /// + /// Do OCR on Image for Text Extraction ... + /// + private async Task PerformOcrAsync( + byte[] imageBytes, + CancellationToken cancellationToken + ) + { + // + // TODO: Complete this ... + return await Task.FromResult(string.Empty); + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPdfFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Text; +using PdfiumViewer; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from PDF files using PdfPig ... + /// + public class XPdfFileContentExtractor : IXPdfFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "application/pdf" + ]; + + /// + /// Minimum Length for Fallback Using Images ... + /// + private const int MinTextLengthThreshold = 50; + + /// + /// Max Pages for Falling Back Using Images ... + /// + private const int MaxPagesForImageFallback = 10; + + /// + /// Default DPI for Rendering Images ... + /// + private const int RenderDpi = 150; + + /// + /// Max Rendered Image Width ... + /// + private const int MaxImageWidth = 2000; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = await ExtractRichAsync( + fileStream, + "document.pdf", + mimeType, + cancellationToken + ); + + // + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // + var extractedText = await ExtractTextAsync( + memoryStream, + cancellationToken + ); + if (IsTextSufficient(extractedText)) + { + // PDF متنی است + result.Text = extractedText; + } + else + { + // PDF اسکن‌شده است - Fallback به تصویر + memoryStream.Position = 0; + result = await FallbackToImageExtractionAsync( + memoryStream, + fileName, + mimeType, + cancellationToken + ); + result.UsedImageFallback = true; + } + + // + return result; + } + + /// + /// Extract Text From PDF Files using Common way ... + /// + /// + /// + /// + private async Task ExtractTextAsync( + Stream pdfStream, + CancellationToken cancellationToken + ) + { + // + var result = await Task.Run(() => + { + // + var sb = new StringBuilder(); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream); + foreach (var page in document.GetPages()) + { + // + var text = page.Text?.Trim(); + if (!string.IsNullOrEmpty(text)) + { + // + sb.AppendLine(text); + sb.AppendLine(); + } + } + + // + return sb.ToString(); + }, cancellationToken); + + // + return result; + } + + /// + /// Check Extracted Content is Sufficient or not ... + /// if not try to Falling Back using Images ... + /// + private bool IsTextSufficient(string text) + { + // + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + // + // حذف فاصله‌ها و بررسی طول + var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // + return cleanText.Length >= MinTextLengthThreshold; + } + + /// + /// Fallback Converts PDF Pages to Images ... + /// + private async Task FallbackToImageExtractionAsync( + Stream pdfStream, + string fileName, + string mimeType, + CancellationToken cancellationToken + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType, + UsedImageFallback = true + }; + + // + await Task.Run(() => + { + // + try + { + // + using var document = PdfDocument.Load(pdfStream); + + // + var pageCount = Math.Min( + document.PageCount, + MaxPagesForImageFallback + ); + + // + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) + { + // + if (cancellationToken.IsCancellationRequested) + { + break; + } + + // + try + { + // + var pageSize = document.PageSizes[pageIndex]; + + // + var scaleFactor = RenderDpi / 72.0; + var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor); + var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor); + + // + if (renderWidth > MaxImageWidth) + { + // + var ratio = (double)MaxImageWidth / renderWidth; + renderWidth = MaxImageWidth; + renderHeight = (int)(renderHeight * ratio); + } + + // + using var bitmap = document.Render( + dpiX: RenderDpi, + dpiY: RenderDpi, + page: pageIndex, + width: renderWidth, + forPrinting: false, + height: renderHeight + ); + + // + byte[] imageBytes; + using (var memoryStream = new MemoryStream()) + { + // + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + imageBytes = memoryStream.ToArray(); + } + + // + result.Images.Add(new XExtractedImage + { + Bytes = imageBytes, + MimeType = "image/png", + PageNumber = pageIndex + 1, + Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})" + }); + } + catch (Exception) + { } + } + } + catch (Exception ex) + { + result.ErrorMessage = $"PDF image extraction failed: {ex.Message}"; + } + }, cancellationToken); + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XPlainTextFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiModels.Models; +using System.Threading.Tasks; +using xAiApi.Interfaces.Extractors; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from plain text files (txt, md, csv, json, xml) ... + /// + public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "text/html", + "text/xml", + "application/json", + "application/xml" + ]; + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + using var reader = new StreamReader(fileStream); + return await reader.ReadToEndAsync(); + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var content = await ExtractAsync( + mimeType: mimeType, + fileStream: fileStream, + cancellationToken: cancellationToken + ); + + // + var result = new XFileExtractionResult + { + Text = content, + FileName = fileName, + MimeType = mimeType + }; + + // + return result; + } + } +} + +### FILE: C:\Users\hkhazaeeasl\Documents\Projets\XProjects\xSaherelmWorkspace\xAiApi\Providers\Extractors\XVisionFileContentExtractor.cs + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using xAiApi.Constants; +using xAiModels.Models; +using xAiApi.Extensions; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiModels.Extensions; +using xAiApi.Configurations; +using xExceptions.Constants; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using System.Collections.Generic; +using xAiApi.Interfaces.Extractors; +using System.Runtime.CompilerServices; + +namespace xAiApi.Providers.Extractors +{ + /// + /// Extracts content from files using Vision Model ... + /// + public class XVisionFileContentExtractor : IXVisionFileContentExtractor + { + /// + /// Supported MIME Types ... + /// + private static readonly string[] SupportedMimeTypes = + [ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/bmp", + "application/pdf", + ]; + + // + private readonly string prompt; + private readonly XAiModelDescriptor descriptor; + + public XVisionFileContentExtractor( + XAiApiConfiguration configuration, + string promptName = XAiApiConstants.XAiApiContentExtractionPromptName, + string modelName = XAiApiConstants.XAiDefaultVisionModelName + ) + { + // + // Retrieve Descriptoir ... + if (modelName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + descriptor = configuration.GetModel(modelName); + + // + // Retrieve Extraction Prompt ... + if (promptName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + prompt = configuration.GetPrompt( + name: promptName, + @params: null + ); + + // + // Validate Requirements ... + if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault()) + { + XException.InvalidData.Throw(); + } + } + + /// + /// Check if this extractor supports the specified MIME type ... + /// + public bool CanExtract(string mimeType) + { + // + return SupportedMimeTypes.Contains( + mimeType?.ToLowerInvariant() ?? string.Empty + ); + } + + /// + /// Extract text content from file stream ... + /// + public async Task ExtractAsync( + Stream fileStream, + string mimeType, + CancellationToken cancellationToken = default + ) + { + var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken); + return result.Text; + } + + /// + /// Extract content from stream as Rich Result ... + /// + /// + /// + /// + /// + /// + public async Task ExtractRichAsync( + Stream fileStream, + string fileName, + string mimeType, + CancellationToken cancellationToken = default + ) + { + // + var result = new XFileExtractionResult + { + FileName = fileName, + MimeType = mimeType + }; + + // + try + { + // + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, cancellationToken); + var imageBytes = memoryStream.ToArray(); + var messages = new[] + { + // + new ChatMessage(ChatRole.User, new[] + { + new DataContent(imageBytes, mimeType) + }) + }; + + // + var extractedContent = await RequestOCRAsync( + messages: messages, + cancellationToken: cancellationToken + ); + if (!extractedContent.IsNullOrEmpty()) + { + result.Text = extractedContent.Trim(); + } + else + { + result.ErrorMessage = "there is not any Extracted Content ..."; + } + } + catch (Exception ex) + { + result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ..."; + } + + // + return result; + } + + // + #region OCR ... + /// + /// Request for Doing OCR on Given Data Contents ... + /// + /// + /// + /// + public virtual async Task RequestOCRAsync( + IList messages, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = descriptor.GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var response = await client.GetResponseAsync( + messages: messages, + cancellationToken: cancellationToken + ); + + // + // Validate Response ... + if (!response.IsValid()) + { + // + // Dispose Client ... + client.Dispose(); + XException.ActionFailed.Throw(); + } + + // + // Retrieve Response Text ... + var result = response.Text; + + // + return result; + } + + /// + /// Request for Doing OCR on Given Data Contents as Stream ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable RequestOCRAsEnumerable( + IList messages, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!messages.HasChild()) + { + XException.InvalidArgs.Throw(); + } + + // + using var client = descriptor.GetClient(); + + // + // Preparing Extraction Prompt Message ... + var pMessage = new ChatMessage( + ChatRole.System, + prompt + ); + + // + messages = [pMessage, .. messages]; + + // + var enumerable = client.GetStreamingResponseAsync( + options: null, + messages: messages + ); + + // + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + yield return res.Text; + } + } + #endregion + } +} diff --git a/Modules/xAiService b/Modules/xAiService index a7e874d..8e88115 160000 --- a/Modules/xAiService +++ b/Modules/xAiService @@ -1 +1 @@ -Subproject commit a7e874d4784e5e1199ddc0a44471feacfb32e5b6 +Subproject commit 8e88115b1f9cdb51b5202659a7a65b2e91486093 diff --git a/xAiApi b/xAiApi index c30d93b..7cad889 160000 --- a/xAiApi +++ b/xAiApi @@ -1 +1 @@ -Subproject commit c30d93be0b3b62b5d6547e7f1ab1cc0decf1ff5c +Subproject commit 7cad889cd043cc047b0082952b05cc041f698753