Files
xSaherelmWorkspace/Documents/Docs/Developer/2.html
T
2026-10-01 01:16:52 +03:30

1485 lines
53 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>طراحی قابلیت File-Attached Messaging در xAiApi - فن آوران ساحر علم</title>
<style>
:root {
--primary: #1e3a8a;
--secondary: #3b82f6;
--accent: #f59e0b;
--success: #10b981;
--danger: #ef4444;
--warning: #f97316;
--bg-light: #f8fafc;
--bg-code: #1e293b;
--text-dark: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Tahoma', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%);
color: var(--text-dark);
line-height: 1.8;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 40px;
text-align: center;
position: relative;
}
.header h1 { font-size: 2.2em; margin-bottom: 10px; position: relative; }
.header .subtitle { font-size: 1.1em; opacity: 0.95; position: relative; }
.meta-bar {
display: flex;
justify-content: space-between;
background: var(--bg-light);
padding: 15px 30px;
border-bottom: 2px solid var(--border);
flex-wrap: wrap;
gap: 15px;
}
.meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9em;
color: var(--text-muted);
}
.meta-item strong { color: var(--primary); }
.content { padding: 40px; }
.section {
margin-bottom: 35px;
padding: 25px;
background: var(--bg-light);
border-radius: 12px;
border-right: 5px solid var(--secondary);
}
.section h2 {
color: var(--primary);
font-size: 1.6em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.section h3 {
color: var(--secondary);
font-size: 1.2em;
margin: 20px 0 12px;
}
.step {
background: white;
padding: 20px;
margin: 15px 0;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border-right: 4px solid var(--accent);
}
.step-number {
display: inline-block;
background: var(--accent);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
text-align: center;
line-height: 32px;
font-weight: bold;
margin-left: 10px;
}
.arch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin: 20px 0;
}
.arch-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border-top: 4px solid var(--secondary);
transition: transform 0.3s;
}
.arch-card:hover { transform: translateY(-5px); }
.arch-card h4 {
color: var(--primary);
margin-bottom: 12px;
font-size: 1.15em;
}
.arch-card ul { list-style: none; padding-right: 0; }
.arch-card li {
padding: 6px 0;
padding-right: 20px;
position: relative;
font-size: 0.95em;
}
.arch-card li::before {
content: '▸';
position: absolute;
right: 0;
color: var(--accent);
font-weight: bold;
}
.tech-badge {
display: inline-block;
background: var(--secondary);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
margin: 3px;
}
.tech-badge.primary { background: var(--primary); }
.tech-badge.success { background: var(--success); }
.tech-badge.accent { background: var(--accent); }
.tech-badge.danger { background: var(--danger); }
.tech-badge.warning { background: var(--warning); }
pre {
background: var(--bg-code);
color: #e2e8f0;
padding: 18px;
border-radius: 8px;
overflow-x: auto;
direction: ltr;
text-align: left;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 0.85em;
margin: 15px 0;
border-right: 4px solid var(--accent);
}
code {
background: #fef3c7;
color: #92400e;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 0.9em;
direction: ltr;
display: inline-block;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
th {
background: var(--primary);
color: white;
padding: 12px;
text-align: right;
font-weight: bold;
}
td {
padding: 12px;
border-bottom: 1px solid var(--border);
}
tr:hover { background: var(--bg-light); }
.flow-diagram {
background: white;
padding: 25px;
border-radius: 10px;
margin: 20px 0;
text-align: center;
}
.flow-step {
display: inline-block;
background: var(--secondary);
color: white;
padding: 10px 20px;
border-radius: 8px;
margin: 5px;
font-size: 0.9em;
}
.flow-arrow {
display: inline-block;
color: var(--accent);
font-size: 1.5em;
margin: 0 8px;
vertical-align: middle;
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin: 15px 0;
border-right: 4px solid;
}
.alert-info {
background: #dbeafe;
border-color: var(--secondary);
color: #1e40af;
}
.alert-success {
background: #d1fae5;
border-color: var(--success);
color: #065f46;
}
.alert-warning {
background: #fef3c7;
border-color: var(--accent);
color: #92400e;
}
.alert-danger {
background: #fee2e2;
border-color: var(--danger);
color: #991b1b;
}
.footer {
background: var(--primary);
color: white;
padding: 25px;
text-align: center;
margin-top: 40px;
}
.footer p { margin: 5px 0; }
.highlight {
background: linear-gradient(120deg, #fef3c7 0%, #fef3c7 100%);
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
}
.toc {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
border: 2px solid var(--border);
}
.toc h3 { color: var(--primary); margin-bottom: 15px; }
.toc ol { padding-right: 25px; }
.toc li { padding: 6px 0; }
.toc a {
color: var(--secondary);
text-decoration: none;
transition: color 0.2s;
}
.toc a:hover { color: var(--primary); text-decoration: underline; }
.file-change {
background: #f0f9ff;
border-right: 4px solid var(--secondary);
padding: 15px;
margin: 10px 0;
border-radius: 8px;
}
.file-change .path {
font-family: 'Consolas', monospace;
color: var(--primary);
font-weight: bold;
direction: ltr;
display: inline-block;
}
.badge-new {
background: var(--success);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75em;
margin-right: 8px;
}
.badge-modify {
background: var(--warning);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75em;
margin-right: 8px;
}
.badge-new-entity {
background: #8b5cf6;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75em;
margin-right: 8px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📎 طراحی قابلیت File-Attached Messaging</h1>
<div class="subtitle">افزودن قابلیت ضمیمه کردن فایل به پیام‌ها برای استفاده در تولید پاسخ توسط مدل هوش مصنوعی</div>
</div>
<div class="meta-bar">
<div class="meta-item">👨‍💻 <strong>توسعه‌دهنده:</strong> هادی خزاعی اصل</div>
<div class="meta-item">🏢 <strong>شرکت:</strong> فن آوران ساحر علم</div>
<div class="meta-item">📅 <strong>تاریخ تهیه مستند:</strong> چهارشنبه ۸ مهر ۱۴۰۵</div>
<div class="meta-item">📦 <strong>پروژه:</strong> xAiApi</div>
</div>
<div class="content">
<div class="toc">
<h3>📑 فهرست مطالب</h3>
<ol>
<li><a href="#analysis">تحلیل وضعیت فعلی و TODO های موجود</a></li>
<li><a href="#strategy">انتخاب استراتژی مناسب</a></li>
<li><a href="#architecture">معماری پیشنهادی</a></li>
<li><a href="#step1">گام ۱: افزودن Entity برای رابطه فایل و پیام</a></li>
<li><a href="#step2">گام ۲: ایجاد File Content Extractor</a></li>
<li><a href="#step3">گام ۳: اصلاح Controller برای دریافت فایل</a></li>
<li><a href="#step4">گام ۴: اصلاح XAIServiceBase برای پردازش فایل</a></li>
<li><a href="#step5">گام ۵: اصلاح Extension برای تبدیل به ChatMessage</a></li>
<li><a href="#step6">گام ۶: ثبت سرویس‌های جدید در DI</a></li>
<li><a href="#step7">گام ۷: Migration پایگاه داده</a></li>
<li><a href="#flow">جریان کامل پردازش</a></li>
<li><a href="#summary">خلاصه تغییرات</a></li>
</ol>
</div>
<!-- Section 1: Analysis -->
<div class="section" id="analysis">
<h2>🔍 گام ۱: تحلیل وضعیت فعلی و TODO های موجود</h2>
<p>
با بررسی دقیق کد پروژه، مشخص شد که <strong>زیرساخت اولیه</strong> برای این قابلیت از قبل طراحی شده
و فقط نیاز به تکمیل دارد. در نقاط مختلف کد، <span class="highlight">TODO های مشخصی</span> وجود دارد:
</p>
<div class="arch-grid">
<div class="arch-card">
<h4>📍 TODO در Controller</h4>
<p style="font-size: 0.9em; color: var(--text-muted);">
در <code>XAiServiceControllerBase.cs</code> در متدهای <code>Ask</code> و <code>AskStream</code>:
</p>
<pre style="font-size: 0.75em;">// TODO: Reading Fiels Form Collection
// and Attach it ...
var result = await aiService.AskAsync(
files: null, // ❌ null ارسال می‌شود
...
);</pre>
</div>
<div class="arch-card">
<h4>📍 TODO در Service</h4>
<p style="font-size: 0.9em; color: var(--text-muted);">
در <code>XAIServiceBase.cs</code> در متدهای <code>AskAsync</code> و <code>AskAsEnumerable</code>:
</p>
<pre style="font-size: 0.75em;">// TODO: Parse Files ...
// for Attached them into Prompt
// Message ...</pre>
</div>
<div class="arch-card">
<h4>📍 TODO در Extension</h4>
<p style="font-size: 0.9em; color: var(--text-muted);">
در <code>XAiModelsExtensions.cs</code> در متد <code>ToChatMessages</code>:
</p>
<pre style="font-size: 0.75em;">// TODO: Handle Files Attached here ...
ChatMessage result = null;</pre>
</div>
<div class="arch-card">
<h4>📍 Entity آماده</h4>
<p style="font-size: 0.9em; color: var(--text-muted);">
Entity <code>XAiDocument</code> از قبل طراحی شده و شامل فیلدهای Vector, ContentHash, EmbeddingModel است:
</p>
<pre style="font-size: 0.75em;">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; }
}</pre>
</div>
</div>
<div class="alert alert-info">
<strong>💡 نتیجه‌گیری:</strong> زیرساخت طراحی شده است، فقط نیاز به پیاده‌سازی و اتصال اجزا داریم.
این یک نشانه عالی از طراحی آینده‌نگرانه معماری شماست! 🎯
</div>
</div>
<!-- Section 2: Strategy -->
<div class="section" id="strategy">
<h2>🎯 گام ۲: انتخاب استراتژی مناسب</h2>
<p>
برای افزودن قابلیت فایل به پیام‌ها، سه استراتژی اصلی وجود دارد. با توجه به معماری پروژه و Entity آماده <code>XAiDocument</code>،
<strong>استراتژی ترکیبی</strong> پیشنهاد می‌شود:
</p>
<table>
<tr>
<th>استراتژی</th>
<th>توضیح</th>
<th>کاربرد</th>
<th>پیچیدگی</th>
</tr>
<tr>
<td><span class="tech-badge success">Level 1</span> Inline Context</td>
<td>محتوای فایل مستقیماً به prompt اضافه شود</td>
<td>فایل‌های متنی کوچک (TXT, MD, CSV)</td>
<td>⭐ ساده</td>
</tr>
<tr>
<td><span class="tech-badge accent">Level 2</span> Multi-modal</td>
<td>تصاویر به صورت DataContent ارسال شوند</td>
<td>فایل‌های تصویری (PNG, JPG)</td>
<td>⭐⭐ متوسط</td>
</tr>
<tr>
<td><span class="tech-badge primary">Level 3</span> RAG</td>
<td>فایل Embed شده و بخش‌های مرتبط جستجو شوند</td>
<td>فایل‌های بزرگ (PDF, DOCX)</td>
<td>⭐⭐⭐ پیچیده</td>
</tr>
</table>
<div class="alert alert-success">
<strong>✅ پیشنهاد:</strong> ابتدا <strong>Level 1 و Level 2</strong> را پیاده‌سازی می‌کنیم که بیشترین کاربرد را دارند.
Level 3 (RAG) در آینده با استفاده از Entity <code>XAiDocument</code> و سرویس Embedding موجود قابل افزودن است.
</div>
</div>
<!-- Section 3: Architecture -->
<div class="section" id="architecture">
<h2>🏗️ گام ۳: معماری پیشنهادی</h2>
<h3>۳.۱ اجزای جدید مورد نیاز:</h3>
<div class="arch-grid">
<div class="arch-card">
<h4>📄 XAiFileAttachment Entity</h4>
<ul>
<li>رابطه بین Message و File</li>
<li>ذخیره Metadata فایل</li>
<li>ContentHash برای Deduplication</li>
</ul>
</div>
<div class="arch-card">
<h4>🔧 IFileContentExtractor</h4>
<ul>
<li>استخراج متن از PDF</li>
<li>استخراج متن از DOCX</li>
<li>استخراج متن از TXT/MD</li>
<li>استخراج متن از CSV/Excel</li>
</ul>
</div>
<div class="arch-card">
<h4>🎨 IFileToChatContentConverter</h4>
<ul>
<li>تبدیل فایل به ChatContent</li>
<li>پشتیبانی از TextContent</li>
<li>پشتیبانی از DataContent (Image)</li>
</ul>
</div>
<div class="arch-card">
<h4>📦 XAiFileAttachmentService</h4>
<ul>
<li>مدیریت آپلود فایل</li>
<li>ارتباط با xFileService</li>
<li>ذخیره Attachments</li>
</ul>
</div>
</div>
<h3>۳.۲ جریان کلی پردازش:</h3>
<div class="flow-diagram">
<span class="flow-step">📤 Upload Files</span>
<span class="flow-arrow">→</span>
<span class="flow-step">💾 Save to Storage</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🔍 Extract Content</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🎨 Convert to ChatContent</span>
<span class="flow-arrow">→</span>
<span class="flow-step">📎 Attach to Message</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🤖 Send to LLM</span>
</div>
</div>
<!-- Step 1: Entity -->
<div class="section" id="step1">
<h2>📝 گام ۴: افزودن Entity برای رابطه فایل و پیام</h2>
<div class="file-change">
<span class="badge-new-entity">NEW ENTITY</span>
<span class="path">xAiModels/Models/Entities/XAiFileAttachment.cs</span>
</div>
<pre>using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using xModels.Base;
namespace xAiModels.Models.Entities
{
/// &lt;summary&gt;
/// Represents a file attached to an AI Message ...
/// &lt;/summary&gt;
public class XAiFileAttachment : XBaseGuidIDEntity
{
/// &lt;summary&gt;
/// User Identifier ...
/// &lt;/summary&gt;
[Required]
[StringLength(255)]
public string OwnerId { get; set; }
/// &lt;summary&gt;
/// Related Message Id ...
/// &lt;/summary&gt;
[Required]
public Guid MessageId { get; set; }
/// &lt;summary&gt;
/// Related File Id (from xFileService) ...
/// &lt;/summary&gt;
[Required]
public Guid FileId { get; set; }
/// &lt;summary&gt;
/// Original File Name ...
/// &lt;/summary&gt;
[Required]
[StringLength(500)]
public string FileName { get; set; }
/// &lt;summary&gt;
/// File MIME Type ...
/// &lt;/summary&gt;
[Required]
[StringLength(255)]
public string MimeType { get; set; }
/// &lt;summary&gt;
/// File Size in Bytes ...
/// &lt;/summary&gt;
public long FileSize { get; set; }
/// &lt;summary&gt;
/// Extracted Text Content (for text-based files) ...
/// &lt;/summary&gt;
public string ExtractedContent { get; set; }
/// &lt;summary&gt;
/// Hash of Extracted Content (for deduplication) ...
/// &lt;/summary&gt;
[StringLength(255)]
public string ContentHash { get; set; }
/// &lt;summary&gt;
/// Processing Status ...
/// &lt;/summary&gt;
public XAiFileProcessingStatus Status { get; set; }
= XAiFileProcessingStatus.Pending;
/// &lt;summary&gt;
/// Error Message (if processing failed) ...
/// &lt;/summary&gt;
public string ErrorMessage { get; set; }
/// &lt;summary&gt;
/// Sequence Order in Message ...
/// &lt;/summary&gt;
public int Order { get; set; }
/// &lt;summary&gt;
/// Created Time ...
/// &lt;/summary&gt;
public DateTime CreatedOn { get; set; }
/// &lt;summary&gt;
/// Updated Time ...
/// &lt;/summary&gt;
public DateTime UpdatedAt { get; set; }
}
/// &lt;summary&gt;
/// File Processing Status ...
/// &lt;/summary&gt;
public enum XAiFileProcessingStatus
{
Pending = 0,
Processing = 1,
Completed = 2,
Failed = 3
}
}</pre>
<div class="file-change">
<span class="badge-new-entity">NEW DTO</span>
<span class="path">xAiModels/Models/Dtos/XAiFileAttachmentDto.cs</span>
</div>
<pre>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; }
}
}</pre>
<div class="alert alert-info">
<strong>💡 نکته طراحی:</strong> با ذخیره <code>ExtractedContent</code> در دیتابیس، از پردازش مجدد فایل در هر پرسش جلوگیری می‌کنیم
و performance بهینه می‌شود.
</div>
</div>
<!-- Step 2: File Content Extractor -->
<div class="section" id="step2">
<h2>🔧 گام ۵: ایجاد File Content Extractor</h2>
<div class="file-change">
<span class="badge-new">NEW</span>
<span class="path">xAiService/Interfaces/IFileContentExtractor.cs</span>
</div>
<pre>using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace xAiService.Interfaces
{
/// &lt;summary&gt;
/// Extracts text content from various file types ...
/// &lt;/summary&gt;
public interface IFileContentExtractor
{
/// &lt;summary&gt;
/// Check if this extractor supports the specified MIME type ...
/// &lt;/summary&gt;
bool CanExtract(string mimeType);
/// &lt;summary&gt;
/// Extract text content from file stream ...
/// &lt;/summary&gt;
Task&lt;string&gt; ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
);
}
}</pre>
<div class="file-change">
<span class="badge-new">NEW</span>
<span class="path">xAiService/Providers/PlainTextContentExtractor.cs</span>
</div>
<pre>using System.IO;
using System.Threading;
using System.Threading.Tasks;
using xAiService.Interfaces;
namespace xAiService.Providers
{
/// &lt;summary&gt;
/// Extracts content from plain text files (txt, md, csv, json, xml) ...
/// &lt;/summary&gt;
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&lt;string&gt; ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
using var reader = new StreamReader(fileStream);
return await reader.ReadToEndAsync();
}
}
}</pre>
<div class="file-change">
<span class="badge-new">NEW</span>
<span class="path">xAiService/Providers/PdfContentExtractor.cs</span>
</div>
<pre>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
{
/// &lt;summary&gt;
/// Extracts content from PDF files using PdfPig ...
/// &lt;/summary&gt;
public class PdfContentExtractor : IFileContentExtractor
{
public bool CanExtract(string mimeType)
{
return mimeType?.ToLowerInvariant() == "application/pdf";
}
public async Task&lt;string&gt; ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
var result = await Task.Run(() =&gt;
{
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;
}
}
}</pre>
<div class="file-change">
<span class="badge-new">NEW</span>
<span class="path">xAiService/Providers/CompositeFileContentExtractor.cs</span>
</div>
<pre>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
{
/// &lt;summary&gt;
/// Composite extractor that delegates to appropriate extractor
/// based on MIME type ...
/// &lt;/summary&gt;
public class CompositeFileContentExtractor : IFileContentExtractor
{
private readonly IEnumerable&lt;IFileContentExtractor&gt; extractors;
public CompositeFileContentExtractor(
IEnumerable&lt;IFileContentExtractor&gt; extractors
)
{
this.extractors = extractors;
}
public bool CanExtract(string mimeType)
{
return extractors.Any(e =&gt; e.CanExtract(mimeType));
}
public async Task&lt;string&gt; ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
var extractor = extractors
.FirstOrDefault(e =&gt; e.CanExtract(mimeType));
if (extractor == null)
{
XException.InvalidData.Throw(
$"Unsupported file type: {mimeType}"
);
}
return await extractor.ExtractAsync(
fileStream,
mimeType,
cancellationToken
);
}
}
}</pre>
<div class="alert alert-warning">
<strong>⚠️ پکیج‌های NuGet مورد نیاز:</strong>
<ul style="padding-right: 25px; margin-top: 10px;">
<li><code>UglyToad.PdfPig</code> - برای استخراج متن از PDF</li>
<li><code>DocumentFormat.OpenXml</code> - برای استخراج متن از DOCX (آینده)</li>
<li><code>ExcelDataReader</code> - برای استخراج متن از Excel (آینده)</li>
</ul>
</div>
</div>
<!-- Step 3: Controller -->
<div class="section" id="step3">
<h2>🎮 گام ۶: اصلاح Controller برای دریافت فایل</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Controllers/XAiServiceControllerBase.cs</span>
</div>
<p><strong>تغییرات در متد <code>Ask</code>:</strong></p>
<pre>// ❌ کد قبلی:
[HttpPost("Ask")]
public async Task&lt;ActionResult&lt;string&gt;&gt; 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&lt;ActionResult&lt;string&gt;&gt; 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);
}
}</pre>
<p><strong>تغییر مشابه برای متد <code>AskStream</code>:</strong></p>
<pre>[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
);
// ...
}</pre>
<div class="alert alert-info">
<strong>💡 نکته مهم:</strong> با تغییر از <code>[FromBody]</code> به <code>[FromForm]</code>،
امکان ارسال همزمان فایل و JSON فراهم می‌شود. توجه داشته باشید که <code>XAiResponseRequest</code>
باید به صورت <code>multipart/form-data</code> ارسال شود.
</div>
<div class="alert alert-warning">
<strong>⚠️ سازگاری با کلاینت‌های موجود:</strong> اگر می‌خواهید کلاینت‌های فعلی که JSON ارسال می‌کنند
همچنان کار کنند، می‌توانید <strong>دو endpoint مجزا</strong> ایجاد کنید:
<ul style="padding-right: 25px; margin-top: 10px;">
<li><code>POST /Ask</code> - برای JSON (بدون فایل)</li>
<li><code>POST /AskWithFiles</code> - برای multipart/form-data (با فایل)</li>
</ul>
</div>
</div>
<!-- Step 4: Service -->
<div class="section" id="step4">
<h2>⚙️ گام ۷: اصلاح XAIServiceBase برای پردازش فایل</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Providers/XAIServiceBase.cs</span>
</div>
<p><strong>۷.۱ افزودن وابستگی‌های جدید به Constructor:</strong></p>
<pre>private readonly IXAiDataProvider dataProvider;
private readonly ILogger&lt;XAIServiceBase&gt; logger;
private readonly XAiApiConfiguration configuration;
private readonly XValidationProvider validationProvider;
// ✅ وابستگی‌های جدید:
private readonly IFileContentExtractor fileContentExtractor;
private readonly IXFileService fileService; // از xFileService
protected XAIServiceBase(
IXAiDataProvider dataProvider,
ILogger&lt;XAIServiceBase&gt; logger,
XAiApiConfiguration configuration,
XValidationProvider validationProvider,
IFileContentExtractor fileContentExtractor, // ✅ جدید
IXFileService fileService, // ✅ جدید
string model = null
)
{
// ...
this.fileContentExtractor = fileContentExtractor;
this.fileService = fileService;
// ...
}</pre>
<p><strong>۷.۲ پیاده‌سازی متد کمکی برای پردازش فایل‌ها:</strong></p>
<pre>#region File Processing ...
/// &lt;summary&gt;
/// Process uploaded files and convert to ChatContents ...
/// &lt;/summary&gt;
protected virtual async Task&lt;IList&lt;AIContent&gt;&gt; ProcessFilesAsync(
IFormFileCollection files,
string ownerId,
Guid messageId,
CancellationToken cancellationToken = default
)
{
var result = new List&lt;AIContent&gt;();
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&lt;byte[]&gt; ReadStreamAsync(
Stream stream,
CancellationToken cancellationToken
)
{
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, cancellationToken);
return memoryStream.ToArray();
}
#endregion</pre>
<p><strong>۷.۳ اصلاح متد <code>AskAsync</code> برای استفاده از فایل‌ها:</strong></p>
<pre>public async Task&lt;XAiMessageDto&gt; 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&lt;ChatMessage&gt; 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
);
// ... (بقیه کد)
}</pre>
</div>
<!-- Step 5: Extension -->
<div class="section" id="step5">
<h2>🔌 گام ۸: اصلاح Extension برای تبدیل به ChatMessage</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiModels/Extensions/XAiModelsExtensions.cs</span>
</div>
<pre>// ❌ کد قبلی:
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&lt;AIContent&gt; additionalContents = null
)
{
ChatMessage result = null;
if (!source.IsNullOrDefault())
{
// ساخت لیست Contents
var contents = new List&lt;AIContent&gt;();
// افزودن متن اصلی پیام
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;
}</pre>
<div class="alert alert-success">
<strong>✅ نکته کلیدی:</strong> با استفاده از <code>AIContent</code> به عنوان کلاس پایه،
می‌توانیم هم <code>TextContent</code> (برای فایل‌های متنی) و هم <code>DataContent</code>
(برای تصاویر) را در یک <code>ChatMessage</code> قرار دهیم. این رویکرد با استاندارد
<code>Microsoft.Extensions.AI</code> کاملاً سازگار است.
</div>
</div>
<!-- Step 6: DI -->
<div class="section" id="step6">
<h2>🔗 گام ۹: ثبت سرویس‌های جدید در DI</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Startup.cs</span>
</div>
<pre>public void ConfigureServices(IServiceCollection services)
{
// ... (کدهای قبلی)
// ✅ ثبت File Content Extractors
services.AddSingleton&lt;IFileContentExtractor, PlainTextContentExtractor&gt;();
services.AddSingleton&lt;IFileContentExtractor, PdfContentExtractor&gt;();
// services.AddSingleton&lt;IFileContentExtractor, DocxContentExtractor&gt;(); // آینده
// ✅ ثبت Composite Extractor
services.AddSingleton&lt;IFileContentExtractor, CompositeFileContentExtractor&gt;();
// ✅ ثبت File Attachment Service
services.AddScoped&lt;IXAiFileAttachmentService, XAiFileAttachmentService&gt;();
// ... (بقیه کدها)
}</pre>
<div class="alert alert-info">
<strong>💡 نکته:</strong> از <code>AddSingleton</code> برای Extractors استفاده می‌کنیم چون stateless هستند
و performance بهتری دارند.
</div>
</div>
<!-- Step 7: Migration -->
<div class="section" id="step7">
<h2>🗄️ گام ۱۰: Migration پایگاه داده</h2>
<p><strong>اجرای دستورات EF Core:</strong></p>
<pre># در 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</pre>
<p><strong>ساختار جدول جدید:</strong></p>
<pre>migrationBuilder.CreateTable(
name: "AiFileAttachments",
columns: table =&gt; new
{
Id = table.Column&lt;Guid&gt;(nullable: false),
Deleted = table.Column&lt;bool&gt;(nullable: false),
OwnerId = table.Column&lt;string&gt;(maxLength: 255, nullable: false),
MessageId = table.Column&lt;Guid&gt;(nullable: false),
FileId = table.Column&lt;Guid&gt;(nullable: false),
FileName = table.Column&lt;string&gt;(maxLength: 500, nullable: false),
MimeType = table.Column&lt;string&gt;(maxLength: 255, nullable: false),
FileSize = table.Column&lt;long&gt;(nullable: false),
ExtractedContent = table.Column&lt;string&gt;(nullable: true),
ContentHash = table.Column&lt;string&gt;(maxLength: 255, nullable: true),
Status = table.Column&lt;int&gt;(nullable: false),
ErrorMessage = table.Column&lt;string&gt;(nullable: true),
Order = table.Column&lt;int&gt;(nullable: false),
CreatedOn = table.Column&lt;DateTime&gt;(nullable: false),
UpdatedAt = table.Column&lt;DateTime&gt;(nullable: false)
},
constraints: table =&gt;
{
table.PrimaryKey("PK_AiFileAttachments", x =&gt; x.Id);
}
);</pre>
</div>
<!-- Flow -->
<div class="section" id="flow">
<h2>🔄 گام ۱۱: جریان کامل پردازش</h2>
<h3>۱۱.۱ جریان پرسش با فایل ضمیمه:</h3>
<div class="flow-diagram">
<span class="flow-step">📤 Client: multipart/form-data</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🎮 Controller: Ask</span>
<span class="flow-arrow">→</span>
<span class="flow-step">⚙️ Service: AskAsync</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🔍 Extract Content</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🎨 Convert to AIContent</span>
<span class="flow-arrow">→</span>
<span class="flow-step">📎 Attach to ChatMessage</span>
<span class="flow-arrow">→</span>
<span class="flow-step">🤖 LLM API</span>
</div>
<h3>۱۱.۲ نمونه درخواست از کلاینت:</h3>
<pre>// 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();</pre>
<pre>// 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"</pre>
</div>
<!-- Summary -->
<div class="section" id="summary">
<h2>📋 گام ۱۲: خلاصه تغییرات</h2>
<table>
<tr>
<th>ردیف</th>
<th>فایل</th>
<th>نوع تغییر</th>
<th>توضیح</th>
</tr>
<tr>
<td>۱</td>
<td><code>xAiModels/Models/Entities/XAiFileAttachment.cs</code></td>
<td><span class="badge-new-entity">NEW</span></td>
<td>Entity جدید برای رابطه فایل و پیام</td>
</tr>
<tr>
<td>۲</td>
<td><code>xAiModels/Models/Dtos/XAiFileAttachmentDto.cs</code></td>
<td><span class="badge-new-entity">NEW</span></td>
<td>DTO متناظر</td>
</tr>
<tr>
<td>۳</td>
<td><code>xAiService/Interfaces/IFileContentExtractor.cs</code></td>
<td><span class="badge-new">NEW</span></td>
<td>Interface برای Extractor</td>
</tr>
<tr>
<td>۴</td>
<td><code>xAiService/Providers/PlainTextContentExtractor.cs</code></td>
<td><span class="badge-new">NEW</span></td>
<td>Extractor برای فایل‌های متنی</td>
</tr>
<tr>
<td>۵</td>
<td><code>xAiService/Providers/PdfContentExtractor.cs</code></td>
<td><span class="badge-new">NEW</span></td>
<td>Extractor برای PDF</td>
</tr>
<tr>
<td>۶</td>
<td><code>xAiService/Providers/CompositeFileContentExtractor.cs</code></td>
<td><span class="badge-new">NEW</span></td>
<td>Composite Pattern برای Extractors</td>
</tr>
<tr>
<td>۷</td>
<td><code>xAiApi/Controllers/XAiServiceControllerBase.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>تغییر به [FromForm] و دریافت files</td>
</tr>
<tr>
<td>۸</td>
<td><code>xAiApi/Providers/XAIServiceBase.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>افزودن ProcessFilesAsync</td>
</tr>
<tr>
<td>۹</td>
<td><code>xAiModels/Extensions/XAiModelsExtensions.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>پشتیبانی از additionalContents</td>
</tr>
<tr>
<td>۱۰</td>
<td><code>xAiApi/Startup.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>ثبت سرویس‌های جدید در DI</td>
</tr>
<tr>
<td>۱۱</td>
<td><code>xAiApi/Database/XAiApiDbContext.cs</code></td>
<td><span class="badge-modify">MODIFY</span></td>
<td>افزودن EntityRegistrar جدید</td>
</tr>
<tr>
<td>۱۲</td>
<td>Migration جدید</td>
<td><span class="badge-new">NEW</span></td>
<td>ایجاد جدول AiFileAttachments</td>
</tr>
</table>
<div class="alert alert-success">
<strong>✅ مزایای این طراحی:</strong>
<ul style="padding-right: 25px; margin-top: 10px;">
<li>🎯 <strong>سازگاری با معماری موجود:</strong> از الگوهای Repository, Provider, Enricher استفاده می‌کند</li>
<li>🔌 <strong>قابل گسترش:</strong> با اضافه کردن Extractor جدید، انواع فایل بیشتری پشتیبانی می‌شود</li>
<li>⚡ <strong>بهینه:</strong> محتوای استخراج شده cache می‌شود</li>
<li>🎨 <strong>Multi-modal Ready:</strong> از تصاویر و فایل‌های متنی پشتیبانی می‌کند</li>
<li>🔒 <strong>امن:</strong> از xFileService موجود برای ذخیره‌سازی استفاده می‌کند</li>
<li>📊 <strong>قابل ردیابی:</strong> هر فایل به پیام مرتبط است و metadata کامل ذخیره می‌شود</li>
</ul>
</div>
<div class="alert alert-warning">
<strong>⚠️ موارد پیشنهادی برای فازهای بعدی:</strong>
<ul style="padding-right: 25px; margin-top: 10px;">
<li>📚 <strong>RAG Implementation:</strong> استفاده از <code>XAiDocument</code> و <code>VectorHelper</code> موجود برای جستجوی معنایی</li>
<li>📄 <strong>DOCX/Excel Support:</strong> افزودن Extractor برای Office files</li>
<li>🔍 <strong>File Preview:</strong> API برای دریافت thumbnail و preview فایل‌ها</li>
<li>📏 <strong>File Size Limits:</strong> پیکربندی حداکثر حجم فایل در <code>XAiApiConfiguration</code></li>
<li>🦠 <strong>Virus Scanning:</strong> اسکن فایل‌های آپلود شده قبل از پردازش</li>
<li>📊 <strong>Analytics:</strong> آمار استفاده از فایل‌ها در مکالمات</li>
</ul>
</div>
<div class="alert alert-info">
<strong>💬 پیام به استاد:</strong> این طراحی کاملاً با معماری ماژولار پروژه شما سازگار است
و از الگوهای موجود (Repository, Provider, Enricher, DI Extensions) استفاده می‌کند.
پیاده‌سازی گام به گام این تغییرات، قابلیت قدرتمند File-Attached Messaging را به پروژه اضافه می‌کند
و زمینه را برای RAG پیشرفته در آینده فراهم می‌سازد. 🚀
</div>
</div>
</div>
<div class="footer">
<p><strong>👨‍💻 توسعه‌دهنده:</strong> هادی خزاعی اصل</p>
<p><strong>🏢 شرکت:</strong> فن آوران ساحر علم</p>
<p><strong>📅 تاریخ تهیه مستند:</strong> چهارشنبه ۸ مهر ۱۴۰۵</p>
<p style="margin-top: 15px; opacity: 0.8; font-size: 0.9em;">
📎 طراحی قابلیت File-Attached Messaging در xAiApi - تمامی حقوق محفوظ است
</p>
</div>
</div>
</body>
</html>