📑 فهرست مطالب
- تحلیل وضعیت فعلی و TODO های موجود
- انتخاب استراتژی مناسب
- معماری پیشنهادی
- گام ۱: افزودن Entity برای رابطه فایل و پیام
- گام ۲: ایجاد File Content Extractor
- گام ۳: اصلاح Controller برای دریافت فایل
- گام ۴: اصلاح XAIServiceBase برای پردازش فایل
- گام ۵: اصلاح Extension برای تبدیل به ChatMessage
- گام ۶: ثبت سرویسهای جدید در DI
- گام ۷: Migration پایگاه داده
- جریان کامل پردازش
- خلاصه تغییرات
🔍 گام ۱: تحلیل وضعیت فعلی و 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) | ⭐⭐⭐ پیچیده |
XAiDocument و سرویس Embedding موجود قابل افزودن است.
🏗️ گام ۳: معماری پیشنهادی
۳.۱ اجزای جدید مورد نیاز:
📄 XAiFileAttachment Entity
- رابطه بین Message و File
- ذخیره Metadata فایل
- ContentHash برای Deduplication
🔧 IFileContentExtractor
- استخراج متن از PDF
- استخراج متن از DOCX
- استخراج متن از TXT/MD
- استخراج متن از CSV/Excel
🎨 IFileToChatContentConverter
- تبدیل فایل به ChatContent
- پشتیبانی از TextContent
- پشتیبانی از DataContent (Image)
📦 XAiFileAttachmentService
- مدیریت آپلود فایل
- ارتباط با xFileService
- ذخیره Attachments
۳.۲ جریان کلی پردازش:
📝 گام ۴: افزودن Entity برای رابطه فایل و پیام
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
}
}
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
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
);
}
}
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();
}
}
}
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;
}
}
}
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
);
}
}
}
UglyToad.PdfPig- برای استخراج متن از PDFDocumentFormat.OpenXml- برای استخراج متن از DOCX (آینده)ExcelDataReader- برای استخراج متن از Excel (آینده)
🎮 گام ۶: اصلاح Controller برای دریافت فایل
تغییرات در متد 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 ارسال شود.
POST /Ask- برای JSON (بدون فایل)POST /AskWithFiles- برای multipart/form-data (با فایل)
⚙️ گام ۷: اصلاح XAIServiceBase برای پردازش فایل
۷.۱ افزودن وابستگیهای جدید به 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
// ❌ کد قبلی:
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
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);
}
);
🔄 گام ۱۱: جریان کامل پردازش
۱۱.۱ جریان پرسش با فایل ضمیمه:
۱۱.۲ نمونه درخواست از کلاینت:
// 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: آمار استفاده از فایلها در مکالمات