📎 پیاده‌سازی استخراج محتوا با ماژول xFileService

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

🎯 گام ۱: استراتژی یکپارچه‌سازی

به جای مدیریت مستقیم IFormFile در لایه سرویس هوش مصنوعی، از معماری تمیز (Clean Architecture) پیروی می‌کنیم:

۱. لایه ارائه (Controller)

  • دریافت IFormFileCollection
  • فراخوانی IXFileProvider.Upload
  • دریافت لیست XFileDto

۲. لایه سرویس (XAiServiceBase)

  • دریافت IEnumerable<XFileDto>
  • فراخوانی IXFileProvider.GetFileDescriptor
  • خواندن Stream و تبدیل به AIContent

۳. لایه داده (Metadata)

  • ذخیره FileId در فیلد MetaDatas پیام
  • حفظ رابطه بدون نیاز به جدول Join جدید

🎮 گام ۲: به‌روزرسانی Controller برای آپلود فایل

ابتدا باید فایل‌ها را از طریق XFileProvider آپلود کنیم تا XFileDto دریافت شود.

MODIFY xAiApi/Controllers/XAiServiceControllerBase.cs
// ۱. افزودن وابستگی به Constructor
private readonly IXFileProvider _fileProvider;

protected XAiServiceControllerBase(
    // ... پارامترهای قبلی
    IXFileProvider fileProvider // ✅ جدید
) : base(...) 
{
    // ...
    _fileProvider = fileProvider;
}

// ۲. اصلاح متد Ask
[HttpPost("Ask")]
[Consumes("multipart/form-data")]
public async Task<ActionResult<string>> Ask(
    [FromForm] XAiResponseRequest request,
    [FromForm] IFormFileCollection files,
    CancellationToken cancellationToken = default
)
{
    try
    {
        if (!request.IsValid()) XException.InvalidArgs.Throw();

        var userInfo = await GetUserInfo();
        var connectionId = GetConnectionId();

        // ✅ آپلود فایل‌ها از طریق ماژول xFileService
        IEnumerable<XFileDto> uploadedFiles = null;
        if (files != null && files.Any())
        {
            uploadedFiles = await _fileProvider.Upload(
                files: files,
                userInfo: userInfo,
                connectionId: connectionId,
                cancellationToken: cancellationToken
            );
        }

        // ✅ ارسال XFileDto به سرویس هوش مصنوعی
        var result = await aiService.AskAsync(
            prompt: request.Prompt,
            ownerId: userInfo.UserId,
            projectId: request.ProjectId,
            conversationId: request.ConversationId,
            connectionId: connectionId,
            attachedFiles: uploadedFiles, // ✅ تغییر نوع پارامتر
            cancellationToken: cancellationToken
        );

        return Ok(result);
    }
    catch (Exception ex)
    {
        return GetExceptionActionResult(ex);
    }
}
💡 نکته: همین تغییر باید برای متد AskStream نیز اعمال شود.

⚙️ گام ۳: تزریق IXFileProvider به XAiServiceBase

MODIFY xAiApi/Providers/XAIServiceBase.cs
// ۱. افزودن فیلد و تزریق در Constructor
private readonly IXFileProvider _fileProvider;

protected XAIServiceBase(
    IXAiDataProvider dataProvider,
    ILogger<XAIServiceBase> logger,
    XAiApiConfiguration configuration,
    XValidationProvider validationProvider,
    IXFileProvider fileProvider, // ✅ جدید
    string model = null
)
{
    this.dataProvider = dataProvider;
    this.logger = logger;
    this.configuration = configuration;
    this.validationProvider = validationProvider;
    this._fileProvider = fileProvider; // ✅ مقداردهی
    
    Descriptor = configuration.GetModel(model);
    Options = new ChatOptions();
}

// ۲. به‌روزرسانی امضای متد AskAsync در Interface و Implementation
public async Task<XAiMessageDto> AskAsync(
    string prompt,
    string ownerId,
    Guid projectId,
    Guid conversationId,
    string connectionId = null,
    IEnumerable<XFileDto> attachedFiles = null, // ✅ تغییر از IFormFileCollection
    CancellationToken cancellationToken = default
)
{
    // ... (کدهای اعتبارسنجی و دریافت Project/Conversation)
    
    // ✅ پردازش فایل‌های ضمیمه
    var fileContents = await ProcessAttachedFilesAsync(attachedFiles, cancellationToken);
    
    // ... (ساخت promptMessage)
    
    // ✅ ذخیره ارجاع فایل‌ها در MetaDatas پیام
    if (attachedFiles != null && attachedFiles.Any())
    {
        var fileIds = attachedFiles.Select(f => f.Id).ToList();
        promptMessage.MetaDatas = new Dictionary<string, object> 
        { 
            { "AttachedFileIds", fileIds } 
        }.ToJSON();
    }
    
    promptMessage = await dataProvider.AddMessage(...);
    
    // ✅ الحاق محتوا به ChatMessage
    var promptChatMessage = promptMessage.ToChatMessages(fileContents);
    
    var answer = await AskLLMAsync(history: history, prompt: promptChatMessage, cancellationToken: cancellationToken);
    
    // ... (ذخیره پاسخ و بازگشت نتیجه)
}

🔍 گام ۴: پیاده‌سازی منطق استخراج محتوا از فایل

این متد کمکی درون XAiServiceBase مسئول خواندن فایل از طریق XFileProvider و تبدیل آن به فرمت قابل فهم برای LLM است.

NEW METHOD xAiApi/Providers/XAIServiceBase.cs
/// <summary>
/// پردازش فایل‌های ضمیمه و تبدیل به AIContent
/// </summary>
private async Task<IList<AIContent>> ProcessAttachedFilesAsync(
    IEnumerable<XFileDto> files,
    CancellationToken cancellationToken = default
)
{
    var contents = new List<AIContent>();
    
    if (files == null || !files.Any())
    {
        return contents;
    }
    
    foreach (var file in files)
    {
        try
        {
            // ✅ دریافت استریم فایل از ماژول xFileService
            var descriptor = await _fileProvider.GetFileDescriptor(
                id: file.Id,
                cancellationToken: cancellationToken
            );
            
            if (descriptor == null || descriptor.Stream == null)
            {
                logger.LogWarning("فایل {FileName} یافت نشد یا قابل خواندن نیست.", file.FileName);
                continue;
            }
            
            // ✅ تشخیص نوع فایل و پردازش مناسب
            if (file.Type == XFileType.Image || descriptor.MIMEType.StartsWith("image/"))
            {
                // برای مدل‌های Vision: ارسال به صورت DataContent
                using var memoryStream = new MemoryStream();
                await descriptor.Stream.CopyToAsync(memoryStream, cancellationToken);
                contents.Add(new DataContent(memoryStream.ToArray(), descriptor.MIMEType));
            }
            else
            {
                // برای فایل‌های متنی: خواندن محتوا و الحاق به Prompt
                using var reader = new StreamReader(descriptor.Stream);
                var textContent = await reader.ReadToEndAsync(cancellationToken);
                
                // قالب‌بندی برای درک بهتر مدل از منبع متن
                var formattedText = $"[File: {file.FileName} (Type: {descriptor.MIMEType})]\n{textContent}\n[/File]";
                contents.Add(new TextContent(formattedText));
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "خطا در پردازش فایل ضمیمه: {FileName}", file.FileName);
        }
    }
    
    return contents;
}
✅ مزیت: با استفاده از GetFileDescriptor، ماژول هوش مصنوعی نیازی به دانستن جزئیات سیستم فایل (File System) ندارد و کاملاً از xFileService انتزاع یافته است.

🔌 گام ۵: به‌روزرسانی Extension مدل‌ها

متد ToChatMessages باید بتواند محتوای استخراج شده از فایل‌ها را در کنار متن اصلی پیام قرار دهد.

MODIFY xAiModels/Extensions/XAiModelsExtensions.cs
public static ChatMessage ToChatMessages(
    this XAiMessageDto source,
    IList<AIContent> additionalContents = null // ✅ پارامتر جدید
)
{
    ChatMessage result = null;
    
    if (!source.IsNullOrDefault())
    {
        var contents = new List<AIContent>();
        
        // ۱. افزودن متن اصلی پیام (Prompt کاربر)
        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;
}

🔗 گام ۶: ثبت وابستگی‌ها (Dependency Injection)

اطمینان حاصل کنید که IXFileProvider در کانتینر DI ثبت شده است (که بر اساس فایل‌های ارائه شده، قبلاً در xFileService.DI.XDIHelperExtension انجام شده است). فقط باید اطمینان حاصل کنیم که در xAiApi قابل تزریق است.

MODIFY xAiApi/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ... (سایر ثبت‌ها)
    
    // ✅ اطمینان از ثبت سرویس فایل (اگر قبلاً در ماژول xFileService ثبت نشده، اینجا فراخوانی شود)
    // services.AddXFileService<XAiApiDbContext>(xDataService.Constants.XRepositoryType.EF);
    
    // ✅ به‌روزرسانی ثبت سرویس‌های AI برای تزریق IXFileProvider
    // نکته: چون XAIServiceBase کلاس پایه است، باید در کلاس‌های مشتق شده (مثل XDefaultAiService) تزریق شود.
    
    // مثال برای XDefaultAiService:
    // services.AddScoped<IXDefaultAiService>(sp => new XDefaultAiService(
    //     sp.GetRequiredService<IXAiDataProvider>(),
    //     sp.GetRequiredService<ILogger<XDefaultAiService>>(),
    //     sp.GetRequiredService<XAiApiConfiguration>(),
    //     sp.GetRequiredService<XValidationProvider>(),
    //     sp.GetRequiredService<IXFileProvider>() // ✅ تزریق جدید
    // ));
}
⚠️ توجه: اگر XAiServiceBase را مستقیماً ثبت نمی‌کنید و از کلاس‌های مشتق شده استفاده می‌کنید، باید Constructor آن کلاس‌ها را نیز برای پذیرش IXFileProvider و پاس دادن آن به base(...) به‌روزرسانی کنید.

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

📤 کلاینت: ارسال Form (Prompt + Files) → 🎮 Controller: فراخوانی IXFileProvider.Upload → 💾 xFileService: ذخیره فیزیکی و ثبت در DB → ⚙️ XAiServiceBase: دریافت List<XFileDto> → 🔍 XAiServiceBase: فراخوانی GetFileDescriptor → 📝 تبدیل Stream به TextContent/DataContent → 🤖 ارسال ChatMessage (Text + Files) به LLM → 💾 ذخیره Message با FileIds در MetaDatas

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

ردیف فایل / ماژول نوع تغییر توضیح
۱ xAiApi/Controllers/XAiServiceControllerBase.cs MODIFY افزودن IXFileProvider و فراخوانی Upload قبل از سرویس AI
۲ xAiApi/Interfaces/IXAiServiceBase.cs MODIFY تغییر پارامتر files از IFormFileCollection به IEnumerable<XFileDto>
۳ xAiApi/Providers/XAIServiceBase.cs MODIFY تزریق IXFileProvider و افزودن متد ProcessAttachedFilesAsync
۴ xAiModels/Extensions/XAiModelsExtensions.cs MODIFY پشتیبانی ToChatMessages از additionalContents
۵ xAiApi/Providers/XDefaultAiService.cs (و سایر مشتق‌ها) MODIFY به‌روزرسانی Constructor برای پاس دادن IXFileProvider به کلاس پایه
✅ دستاوردهای این طراحی:
  • 🛡️ جداسازی مسئولیت‌ها: ماژول AI دیگر درگیر آپلود یا مدیریت فایل فیزیکی نیست.
  • ♻️ استفاده مجدد: از تمام قابلیت‌های xFileService (مانند Thumbnail، References، و Storage) بهره می‌بریم.
  • 🔗 ردیابی‌پذیری: با ذخیره FileId در MetaDatas، همیشه می‌توانیم بفهمیم کدام فایل‌ها به کدام پیام متصل بوده‌اند.
  • 🎨 پشتیبانی چندوجهی (Multi-modal): آماده‌سازی برای ارسال تصاویر به صورت DataContent به مدل‌های Vision.