📎 طراحی قابلیت File-Attached Messaging

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

🔍 گام ۱: تحلیل وضعیت فعلی و TODO های موجود

با بررسی دقیق کد پروژه، مشخص شد که زیرساخت اولیه برای این قابلیت از قبل طراحی شده و فقط نیاز به تکمیل دارد. در نقاط مختلف کد، TODO های مشخصی وجود دارد:

📍 TODO در Controller

در XAiServiceControllerBase.cs در متدهای Ask و AskStream:

// TODO: Reading Fiels Form Collection 
// and Attach it ...
var result = await aiService.AskAsync(
    files: null,  // ❌ null ارسال می‌شود
    ...
);

📍 TODO در Service

در XAIServiceBase.cs در متدهای AskAsync و AskAsEnumerable:

// TODO: Parse Files ...
// for Attached them into Prompt 
// Message ...

📍 TODO در Extension

در XAiModelsExtensions.cs در متد ToChatMessages:

// TODO: Handle Files Attached here ...
ChatMessage result = null;

📍 Entity آماده

Entity XAiDocument از قبل طراحی شده و شامل فیلدهای Vector, ContentHash, EmbeddingModel است:

public class XAiDocument : XBaseGuidIDEntity
{
    public string Content { get; set; }
    public string ContentHash { get; set; }
    public string EmbeddingModel { get; set; }
    public int Dimensions { get; set; }
    public byte[] Vector { get; set; }
}
💡 نتیجه‌گیری: زیرساخت طراحی شده است، فقط نیاز به پیاده‌سازی و اتصال اجزا داریم. این یک نشانه عالی از طراحی آینده‌نگرانه معماری شماست! 🎯

🎯 گام ۲: انتخاب استراتژی مناسب

برای افزودن قابلیت فایل به پیام‌ها، سه استراتژی اصلی وجود دارد. با توجه به معماری پروژه و Entity آماده XAiDocument، استراتژی ترکیبی پیشنهاد می‌شود:

استراتژی توضیح کاربرد پیچیدگی
Level 1 Inline Context محتوای فایل مستقیماً به prompt اضافه شود فایل‌های متنی کوچک (TXT, MD, CSV) ⭐ ساده
Level 2 Multi-modal تصاویر به صورت DataContent ارسال شوند فایل‌های تصویری (PNG, JPG) ⭐⭐ متوسط
Level 3 RAG فایل Embed شده و بخش‌های مرتبط جستجو شوند فایل‌های بزرگ (PDF, DOCX) ⭐⭐⭐ پیچیده
✅ پیشنهاد: ابتدا Level 1 و Level 2 را پیاده‌سازی می‌کنیم که بیشترین کاربرد را دارند. Level 3 (RAG) در آینده با استفاده از Entity XAiDocument و سرویس Embedding موجود قابل افزودن است.

🏗️ گام ۳: معماری پیشنهادی

۳.۱ اجزای جدید مورد نیاز:

📄 XAiFileAttachment Entity

  • رابطه بین Message و File
  • ذخیره Metadata فایل
  • ContentHash برای Deduplication

🔧 IFileContentExtractor

  • استخراج متن از PDF
  • استخراج متن از DOCX
  • استخراج متن از TXT/MD
  • استخراج متن از CSV/Excel

🎨 IFileToChatContentConverter

  • تبدیل فایل به ChatContent
  • پشتیبانی از TextContent
  • پشتیبانی از DataContent (Image)

📦 XAiFileAttachmentService

  • مدیریت آپلود فایل
  • ارتباط با xFileService
  • ذخیره Attachments

۳.۲ جریان کلی پردازش:

📤 Upload Files → 💾 Save to Storage → 🔍 Extract Content → 🎨 Convert to ChatContent → 📎 Attach to Message → 🤖 Send to LLM

📝 گام ۴: افزودن Entity برای رابطه فایل و پیام

NEW ENTITY xAiModels/Models/Entities/XAiFileAttachment.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using xModels.Base;

namespace xAiModels.Models.Entities
{
    /// <summary>
    /// Represents a file attached to an AI Message ...
    /// </summary>
    public class XAiFileAttachment : XBaseGuidIDEntity
    {
        /// <summary>
        /// User Identifier ...
        /// </summary>
        [Required]
        [StringLength(255)]
        public string OwnerId { get; set; }

        /// <summary>
        /// Related Message Id ...
        /// </summary>
        [Required]
        public Guid MessageId { get; set; }

        /// <summary>
        /// Related File Id (from xFileService) ...
        /// </summary>
        [Required]
        public Guid FileId { get; set; }

        /// <summary>
        /// Original File Name ...
        /// </summary>
        [Required]
        [StringLength(500)]
        public string FileName { get; set; }

        /// <summary>
        /// File MIME Type ...
        /// </summary>
        [Required]
        [StringLength(255)]
        public string MimeType { get; set; }

        /// <summary>
        /// File Size in Bytes ...
        /// </summary>
        public long FileSize { get; set; }

        /// <summary>
        /// Extracted Text Content (for text-based files) ...
        /// </summary>
        public string ExtractedContent { get; set; }

        /// <summary>
        /// Hash of Extracted Content (for deduplication) ...
        /// </summary>
        [StringLength(255)]
        public string ContentHash { get; set; }

        /// <summary>
        /// Processing Status ...
        /// </summary>
        public XAiFileProcessingStatus Status { get; set; }
            = XAiFileProcessingStatus.Pending;

        /// <summary>
        /// Error Message (if processing failed) ...
        /// </summary>
        public string ErrorMessage { get; set; }

        /// <summary>
        /// Sequence Order in Message ...
        /// </summary>
        public int Order { get; set; }

        /// <summary>
        /// Created Time ...
        /// </summary>
        public DateTime CreatedOn { get; set; }

        /// <summary>
        /// Updated Time ...
        /// </summary>
        public DateTime UpdatedAt { get; set; }
    }

    /// <summary>
    /// File Processing Status ...
    /// </summary>
    public enum XAiFileProcessingStatus
    {
        Pending = 0,
        Processing = 1,
        Completed = 2,
        Failed = 3
    }
}
NEW DTO xAiModels/Models/Dtos/XAiFileAttachmentDto.cs
using System;
using xModels.Base;

namespace xAiModels.Models.Dtos
{
    public class XAiFileAttachmentDto : XBaseGuidIDEntityDto
    {
        public string OwnerId { get; set; }
        public Guid MessageId { get; set; }
        public Guid FileId { get; set; }
        public string FileName { get; set; }
        public string MimeType { get; set; }
        public long FileSize { get; set; }
        public string ExtractedContent { get; set; }
        public string ContentHash { get; set; }
        public XAiFileProcessingStatus Status { get; set; }
        public string ErrorMessage { get; set; }
        public int Order { get; set; }
        public DateTime CreatedOn { get; set; }
        public DateTime UpdatedAt { get; set; }
    }
}
💡 نکته طراحی: با ذخیره ExtractedContent در دیتابیس، از پردازش مجدد فایل در هر پرسش جلوگیری می‌کنیم و performance بهینه می‌شود.

🔧 گام ۵: ایجاد File Content Extractor

NEW xAiService/Interfaces/IFileContentExtractor.cs
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace xAiService.Interfaces
{
    /// <summary>
    /// Extracts text content from various file types ...
    /// </summary>
    public interface IFileContentExtractor
    {
        /// <summary>
        /// Check if this extractor supports the specified MIME type ...
        /// </summary>
        bool CanExtract(string mimeType);

        /// <summary>
        /// Extract text content from file stream ...
        /// </summary>
        Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        );
    }
}
NEW xAiService/Providers/PlainTextContentExtractor.cs
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using xAiService.Interfaces;

namespace xAiService.Providers
{
    /// <summary>
    /// Extracts content from plain text files (txt, md, csv, json, xml) ...
    /// </summary>
    public class PlainTextContentExtractor : IFileContentExtractor
    {
        private static readonly string[] SupportedMimeTypes = new[]
        {
            "text/plain",
            "text/markdown",
            "text/csv",
            "text/html",
            "text/xml",
            "application/json",
            "application/xml"
        };

        public bool CanExtract(string mimeType)
        {
            return SupportedMimeTypes.Contains(
                mimeType?.ToLowerInvariant() ?? string.Empty
            );
        }

        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            using var reader = new StreamReader(fileStream);
            return await reader.ReadToEndAsync();
        }
    }
}
NEW xAiService/Providers/PdfContentExtractor.cs
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using xAiService.Interfaces;
// Install-Package PdfPig
using UglyToad.PdfPig;
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;

namespace xAiService.Providers
{
    /// <summary>
    /// Extracts content from PDF files using PdfPig ...
    /// </summary>
    public class PdfContentExtractor : IFileContentExtractor
    {
        public bool CanExtract(string mimeType)
        {
            return mimeType?.ToLowerInvariant() == "application/pdf";
        }

        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            var result = await Task.Run(() =>
            {
                var sb = new StringBuilder();
                using var document = PdfDocument.Open(fileStream);
                
                foreach (var page in document.GetPages())
                {
                    var text = ContentOrderTextExtractor
                        .GetText(page);
                    sb.AppendLine(text);
                    sb.AppendLine();
                }
                
                return sb.ToString();
            }, cancellationToken);
            
            return result;
        }
    }
}
NEW xAiService/Providers/CompositeFileContentExtractor.cs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using xAiService.Interfaces;
using xExceptions.Constants;

namespace xAiService.Providers
{
    /// <summary>
    /// Composite extractor that delegates to appropriate extractor 
    /// based on MIME type ...
    /// </summary>
    public class CompositeFileContentExtractor : IFileContentExtractor
    {
        private readonly IEnumerable<IFileContentExtractor> extractors;

        public CompositeFileContentExtractor(
            IEnumerable<IFileContentExtractor> extractors
        )
        {
            this.extractors = extractors;
        }

        public bool CanExtract(string mimeType)
        {
            return extractors.Any(e => e.CanExtract(mimeType));
        }

        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            var extractor = extractors
                .FirstOrDefault(e => e.CanExtract(mimeType));
            
            if (extractor == null)
            {
                XException.InvalidData.Throw(
                    $"Unsupported file type: {mimeType}"
                );
            }
            
            return await extractor.ExtractAsync(
                fileStream, 
                mimeType, 
                cancellationToken
            );
        }
    }
}
⚠️ پکیج‌های NuGet مورد نیاز:
  • UglyToad.PdfPig - برای استخراج متن از PDF
  • DocumentFormat.OpenXml - برای استخراج متن از DOCX (آینده)
  • ExcelDataReader - برای استخراج متن از Excel (آینده)

🎮 گام ۶: اصلاح Controller برای دریافت فایل

MODIFY xAiApi/Controllers/XAiServiceControllerBase.cs

تغییرات در متد Ask:

// ❌ کد قبلی:
[HttpPost("Ask")]
public async Task<ActionResult<string>> Ask(
    [FromBody] XAiResponseRequest request,
    CancellationToken cancellationToken = default
)
{
    // ...
    // TODO: Reading Fiels Form Collection and Attach it ...
    var result = await aiService.AskAsync(
        files: null,  // ❌ null
        prompt: request.Prompt,
        // ...
    );
}

// ✅ کد جدید:
[HttpPost("Ask")]
[Consumes("multipart/form-data")]
public async Task<ActionResult<string>> Ask(
    [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,  // ✅ فایل‌ها ارسال می‌شوند
            prompt: request.Prompt,
            ownerId: userInfo.UserId,
            connectionId: connectionId,
            projectId: request.ProjectId,
            cancellationToken: cancellationToken,
            conversationId: request.ConversationId
        );

        return Ok(result);
    }
    catch (Exception ex)
    {
        return GetExceptionActionResult(ex);
    }
}

تغییر مشابه برای متد AskStream:

[HttpPost("AskStream")]
[Consumes("multipart/form-data")]
public async Task AskStream(
    [FromForm] XAiResponseRequest request,
    [FromForm] IFormFileCollection files,
    CancellationToken cancellationToken = default
)
{
    // ...
    var enumerable = aiService.AskAsEnumerable(
        files: files,  // ✅ فایل‌ها ارسال می‌شوند
        prompt: request.Prompt,
        ownerId: userInfo.UserId,
        connectionId: connectionId,
        projectId: request.ProjectId,
        cancellationToken: cancellationToken,
        conversationId: request.ConversationId
    );
    // ...
}
💡 نکته مهم: با تغییر از [FromBody] به [FromForm]، امکان ارسال همزمان فایل و JSON فراهم می‌شود. توجه داشته باشید که XAiResponseRequest باید به صورت multipart/form-data ارسال شود.
⚠️ سازگاری با کلاینت‌های موجود: اگر می‌خواهید کلاینت‌های فعلی که JSON ارسال می‌کنند همچنان کار کنند، می‌توانید دو endpoint مجزا ایجاد کنید:
  • POST /Ask - برای JSON (بدون فایل)
  • POST /AskWithFiles - برای multipart/form-data (با فایل)

⚙️ گام ۷: اصلاح XAIServiceBase برای پردازش فایل

MODIFY xAiApi/Providers/XAIServiceBase.cs

۷.۱ افزودن وابستگی‌های جدید به Constructor:

private readonly IXAiDataProvider dataProvider;
private readonly ILogger<XAIServiceBase> logger;
private readonly XAiApiConfiguration configuration;
private readonly XValidationProvider validationProvider;
// ✅ وابستگی‌های جدید:
private readonly IFileContentExtractor fileContentExtractor;
private readonly IXFileService fileService; // از xFileService

protected XAIServiceBase(
    IXAiDataProvider dataProvider,
    ILogger<XAIServiceBase> logger,
    XAiApiConfiguration configuration,
    XValidationProvider validationProvider,
    IFileContentExtractor fileContentExtractor, // ✅ جدید
    IXFileService fileService,                   // ✅ جدید
    string model = null
)
{
    // ...
    this.fileContentExtractor = fileContentExtractor;
    this.fileService = fileService;
    // ...
}

۷.۲ پیاده‌سازی متد کمکی برای پردازش فایل‌ها:

#region File Processing ...

/// <summary>
/// Process uploaded files and convert to ChatContents ...
/// </summary>
protected virtual async Task<IList<AIContent>> ProcessFilesAsync(
    IFormFileCollection files,
    string ownerId,
    Guid messageId,
    CancellationToken cancellationToken = default
)
{
    var result = new List<AIContent>();
    
    if (files == null || !files.Any())
    {
        return result;
    }
    
    foreach (var file in files)
    {
        if (file.Length == 0) continue;
        
        var mimeType = file.ContentType;
        
        // Check if it's an image (multi-modal)
        if (IsImageFile(mimeType))
        {
            var imageData = await ReadStreamAsync(
                file.OpenReadStream(), 
                cancellationToken
            );
            result.Add(new DataContent(imageData, mimeType));
        }
        // Text-based files
        else if (fileContentExtractor.CanExtract(mimeType))
        {
            using var stream = file.OpenReadStream();
            var content = await fileContentExtractor.ExtractAsync(
                stream, 
                mimeType, 
                cancellationToken
            );
            
            // Add file content as context
            var fileContext = $"[File: {file.FileName}]\n{content}\n[/File]";
            result.Add(new TextContent(fileContext));
        }
        else
        {
            logger.LogWarning(
                "Unsupported file type: {MimeType}", 
                mimeType
            );
        }
    }
    
    return result;
}

private bool IsImageFile(string mimeType)
{
    return mimeType?.StartsWith("image/") == true;
}

private async Task<byte[]> ReadStreamAsync(
    Stream stream,
    CancellationToken cancellationToken
)
{
    using var memoryStream = new MemoryStream();
    await stream.CopyToAsync(memoryStream, cancellationToken);
    return memoryStream.ToArray();
}

#endregion

۷.۳ اصلاح متد AskAsync برای استفاده از فایل‌ها:

public async Task<XAiMessageDto> AskAsync(
    string prompt,
    string ownerId,
    Guid projectId,
    Guid conversationId,
    string connectionId = null,
    IFormFileCollection files = null,
    CancellationToken cancellationToken = default
)
{
    // ... (کدهای قبلی validation و load project/conversation)
    
    // Handle Memory ...
    IList<ChatMessage> 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
    };
    
    promptMessage = await dataProvider.AddMessage(
        item: promptMessage,
        connectionId: connectionId,
        conversationId: conversationId,
        cancellationToken: cancellationToken
    );
    
    // ✅ پردازش فایل‌ها و افزودن به پیام
    var fileContents = await ProcessFilesAsync(
        files: files,
        ownerId: ownerId,
        messageId: promptMessage.Id,
        cancellationToken: cancellationToken
    );
    
    // ✅ ذخیره Attachments در دیتابیس
    if (files != null && files.Any())
    {
        await SaveFileAttachmentsAsync(
            files: files,
            ownerId: ownerId,
            messageId: promptMessage.Id,
            cancellationToken: cancellationToken
        );
    }
    
    // ✅ ساخت ChatMessage با فایل‌های ضمیمه
    var promptChatMessage = promptMessage.ToChatMessages(fileContents);
    
    // Ask Questions From LLM ...
    var answer = await AskLLMAsync(
        history: history,
        prompt: promptChatMessage,
        cancellationToken: cancellationToken
    );
    
    // ... (بقیه کد)
}

🔌 گام ۸: اصلاح Extension برای تبدیل به ChatMessage

MODIFY xAiModels/Extensions/XAiModelsExtensions.cs
// ❌ کد قبلی:
public static ChatMessage ToChatMessages(
    this XAiMessageDto source
)
{
    // TODO: Handle Files Attached here ...
    ChatMessage result = null;
    if (!source.IsNullOrDefault())
    {
        result = new ChatMessage
        {
            AuthorName = ...,
            Role = source.Role.ToChatRole(),
            MessageId = source.Id.ToString(),
            Contents = [new TextContent(source.Content)]
        };
    }
    return result;
}

// ✅ کد جدید با پشتیبانی از فایل:
public static ChatMessage ToChatMessages(
    this XAiMessageDto source,
    IList<AIContent> additionalContents = null
)
{
    ChatMessage result = null;
    
    if (!source.IsNullOrDefault())
    {
        // ساخت لیست Contents
        var contents = new List<AIContent>();
        
        // افزودن متن اصلی پیام
        if (!string.IsNullOrWhiteSpace(source.Content))
        {
            contents.Add(new TextContent(source.Content));
        }
        
        // ✅ افزودن محتوای فایل‌های ضمیمه
        if (additionalContents != null && additionalContents.Any())
        {
            contents.AddRange(additionalContents);
        }
        
        result = new ChatMessage
        {
            AuthorName = 
                source.Role == XAiChatRole.User &&
                !source.Owner.IsNullOrDefault()
                ? source.Owner.GetFullname()
                : string.Empty,
            Role = source.Role.ToChatRole(),
            MessageId = source.Id.ToString(),
            Contents = contents  // ✅ شامل متن + فایل‌ها
        };
    }
    
    return result;
}
✅ نکته کلیدی: با استفاده از AIContent به عنوان کلاس پایه، می‌توانیم هم TextContent (برای فایل‌های متنی) و هم DataContent (برای تصاویر) را در یک ChatMessage قرار دهیم. این رویکرد با استاندارد Microsoft.Extensions.AI کاملاً سازگار است.

🔗 گام ۹: ثبت سرویس‌های جدید در DI

MODIFY xAiApi/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ... (کدهای قبلی)
    
    // ✅ ثبت File Content Extractors
    services.AddSingleton<IFileContentExtractor, PlainTextContentExtractor>();
    services.AddSingleton<IFileContentExtractor, PdfContentExtractor>();
    // services.AddSingleton<IFileContentExtractor, DocxContentExtractor>(); // آینده
    
    // ✅ ثبت Composite Extractor
    services.AddSingleton<IFileContentExtractor, CompositeFileContentExtractor>();
    
    // ✅ ثبت File Attachment Service
    services.AddScoped<IXAiFileAttachmentService, XAiFileAttachmentService>();
    
    // ... (بقیه کدها)
}
💡 نکته: از AddSingleton برای Extractors استفاده می‌کنیم چون stateless هستند و performance بهتری دارند.

🗄️ گام ۱۰: Migration پایگاه داده

اجرای دستورات EF Core:

# در Package Manager Console:
Add-Migration AddFileAttachments -Context XAiApiDbContext
Update-Database -Context XAiApiDbContext

# یا در .NET CLI:
dotnet ef migrations add AddFileAttachments --context XAiApiDbContext
dotnet ef database update --context XAiApiDbContext

ساختار جدول جدید:

migrationBuilder.CreateTable(
    name: "AiFileAttachments",
    columns: table => new
    {
        Id = table.Column<Guid>(nullable: false),
        Deleted = table.Column<bool>(nullable: false),
        OwnerId = table.Column<string>(maxLength: 255, nullable: false),
        MessageId = table.Column<Guid>(nullable: false),
        FileId = table.Column<Guid>(nullable: false),
        FileName = table.Column<string>(maxLength: 500, nullable: false),
        MimeType = table.Column<string>(maxLength: 255, nullable: false),
        FileSize = table.Column<long>(nullable: false),
        ExtractedContent = table.Column<string>(nullable: true),
        ContentHash = table.Column<string>(maxLength: 255, nullable: true),
        Status = table.Column<int>(nullable: false),
        ErrorMessage = table.Column<string>(nullable: true),
        Order = table.Column<int>(nullable: false),
        CreatedOn = table.Column<DateTime>(nullable: false),
        UpdatedAt = table.Column<DateTime>(nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_AiFileAttachments", x => x.Id);
    }
);

🔄 گام ۱۱: جریان کامل پردازش

۱۱.۱ جریان پرسش با فایل ضمیمه:

📤 Client: multipart/form-data → 🎮 Controller: Ask → ⚙️ Service: AskAsync → 🔍 Extract Content → 🎨 Convert to AIContent → 📎 Attach to ChatMessage → 🤖 LLM API

۱۱.۲ نمونه درخواست از کلاینت:

// JavaScript / Fetch API Example:
const formData = new FormData();
formData.append('Prompt', 'این فایل را تحلیل کن');
formData.append('ProjectId', '...');
formData.append('ConversationId', '...');

// افزودن فایل‌ها
const fileInput = document.getElementById('fileInput');
for (const file of fileInput.files) {
    formData.append('files', file);
}

const response = await fetch('/DefaultAi/Ask', {
    method: 'POST',
    body: formData  // ✅ Content-Type خودکار تنظیم می‌شود
});

const result = await response.json();
// cURL Example:
curl -X POST "https://api.example.com/DefaultAi/Ask" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "Prompt=این فایل را تحلیل کن" \
  -F "ProjectId=YOUR_PROJECT_ID" \
  -F "ConversationId=YOUR_CONVERSATION_ID" \
  -F "files=@/path/to/document.pdf" \
  -F "files=@/path/to/image.png"

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

ردیف فایل نوع تغییر توضیح
۱ xAiModels/Models/Entities/XAiFileAttachment.cs NEW Entity جدید برای رابطه فایل و پیام
۲ xAiModels/Models/Dtos/XAiFileAttachmentDto.cs NEW DTO متناظر
۳ xAiService/Interfaces/IFileContentExtractor.cs NEW Interface برای Extractor
۴ xAiService/Providers/PlainTextContentExtractor.cs NEW Extractor برای فایل‌های متنی
۵ xAiService/Providers/PdfContentExtractor.cs NEW Extractor برای PDF
۶ xAiService/Providers/CompositeFileContentExtractor.cs NEW Composite Pattern برای Extractors
۷ xAiApi/Controllers/XAiServiceControllerBase.cs MODIFY تغییر به [FromForm] و دریافت files
۸ xAiApi/Providers/XAIServiceBase.cs MODIFY افزودن ProcessFilesAsync
۹ xAiModels/Extensions/XAiModelsExtensions.cs MODIFY پشتیبانی از additionalContents
۱۰ xAiApi/Startup.cs MODIFY ثبت سرویس‌های جدید در DI
۱۱ xAiApi/Database/XAiApiDbContext.cs MODIFY افزودن EntityRegistrar جدید
۱۲ Migration جدید NEW ایجاد جدول AiFileAttachments
✅ مزایای این طراحی:
  • 🎯 سازگاری با معماری موجود: از الگوهای Repository, Provider, Enricher استفاده می‌کند
  • 🔌 قابل گسترش: با اضافه کردن Extractor جدید، انواع فایل بیشتری پشتیبانی می‌شود
  • ⚡ بهینه: محتوای استخراج شده cache می‌شود
  • 🎨 Multi-modal Ready: از تصاویر و فایل‌های متنی پشتیبانی می‌کند
  • 🔒 امن: از xFileService موجود برای ذخیره‌سازی استفاده می‌کند
  • 📊 قابل ردیابی: هر فایل به پیام مرتبط است و metadata کامل ذخیره می‌شود
⚠️ موارد پیشنهادی برای فازهای بعدی:
  • 📚 RAG Implementation: استفاده از XAiDocument و VectorHelper موجود برای جستجوی معنایی
  • 📄 DOCX/Excel Support: افزودن Extractor برای Office files
  • 🔍 File Preview: API برای دریافت thumbnail و preview فایل‌ها
  • 📏 File Size Limits: پیکربندی حداکثر حجم فایل در XAiApiConfiguration
  • 🦠 Virus Scanning: اسکن فایل‌های آپلود شده قبل از پردازش
  • 📊 Analytics: آمار استفاده از فایل‌ها در مکالمات
💬 پیام به استاد: این طراحی کاملاً با معماری ماژولار پروژه شما سازگار است و از الگوهای موجود (Repository, Provider, Enricher, DI Extensions) استفاده می‌کند. پیاده‌سازی گام به گام این تغییرات، قابلیت قدرتمند File-Attached Messaging را به پروژه اضافه می‌کند و زمینه را برای RAG پیشرفته در آینده فراهم می‌سازد. 🚀