progress in ai

This commit is contained in:
Hesabix 2025-07-22 08:55:13 +00:00
parent 8a3ebc64cb
commit 9b5e7947cd
20 changed files with 1039 additions and 321 deletions

View file

@ -95,3 +95,8 @@ services:
App\Twig\NumberFormatExtension:
tags: ['twig.extension']
App\Cog\PersonService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$access: '@App\Service\Access'

View file

@ -0,0 +1,78 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Person;
use App\Entity\PersonType;
use App\Entity\HesabdariRow;
use App\Service\Explore;
use App\Service\Access;
/**
* سرویس مدیریت اشخاص
*
* این سرویس برای مدیریت عملیات مربوط به اشخاص استفاده می‌شود
*/
class PersonService
{
private EntityManagerInterface $entityManager;
private Access $access;
/**
* سازنده سرویس
*/
public function __construct(EntityManagerInterface $entityManager, Access $access)
{
$this->entityManager = $entityManager;
$this->access = $access;
}
/**
* دریافت اطلاعات شخص
*
* @param string $code کد شخص
* @param array $acc اطلاعات دسترسی
* @return array اطلاعات شخص
* @throws \ReflectionException
*/
public function getPersonInfo(string $code, array $acc): array
{
$person = $this->entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
$types = $this->entityManager->getRepository(PersonType::class)->findAll();
$response = Explore::ExplorePerson($person, $types);
$rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0;
$bd = 0;
foreach ($rows as $row) {
try {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs(); // بستانکار
$bd += (float) $row->getBd(); // بدهکار
}
} catch (\Exception $e) {
// در صورت بروز خطا، این ردیف را نادیده می‌گیریم و به ردیف بعدی می‌رویم
continue;
}
}
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
return $response;
}
}

View file

@ -18,6 +18,7 @@ use App\Entity\Storeroom;
use App\Entity\StoreroomItem;
use App\Entity\StoreroomTicket;
use App\Service\Explore;
use App\Cog\PersonService;
use Doctrine\ORM\EntityManagerInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
@ -61,40 +62,13 @@ class PersonsController extends AbstractController
* @throws \ReflectionException
*/
#[Route('/api/person/info/{code}', name: 'app_persons_info')]
public function app_persons_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
public function app_persons_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PersonService $personService): JsonResponse
{
$acc = $access->hasRole('person');
if (!$acc)
throw $this->createAccessDeniedException();
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
$types = $entityManager->getRepository(PersonType::class)->findAll();
$response = Explore::ExplorePerson($person, $types);
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0;
$bd = 0;
foreach ($rows as $row) {
try {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs(); // بستانکار
$bd += (float) $row->getBd(); // بدهکار
}
} catch (\Exception $e) {
// در صورت بروز خطا، این ردیف را نادیده می‌گیریم و به ردیف بعدی می‌رویم
continue;
}
}
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
$response = $personService->getPersonInfo($code, $acc);
return $this->json($response);
}

View file

@ -35,14 +35,23 @@ class wizardController extends AbstractController
try {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
return $this->json([
'success' => false,
'error' => 'دسترسی غیرمجاز',
'debug_info' => [
'access' => $acc
]
]);
}
// بررسی دسترسی هوش مصنوعی
if (!$acc['ai']) {
return $this->json([
'success' => false,
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید'
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید',
'debug_info' => [
'ai_access' => $acc['ai']
]
]);
}
@ -51,7 +60,10 @@ class wizardController extends AbstractController
if (!isset($params['message']) || empty($params['message'])) {
return $this->json([
'success' => false,
'error' => 'پیام الزامی است'
'error' => 'پیام الزامی است',
'debug_info' => [
'params' => $params
]
]);
}
@ -64,32 +76,35 @@ class wizardController extends AbstractController
if (!$aiStatus['isEnabled']) {
return $this->json([
'success' => false,
'error' => 'سرویس هوش مصنوعی غیرفعال است'
'error' => 'سرویس هوش مصنوعی غیرفعال است',
'debug_info' => [
'ai_status' => $aiStatus
]
]);
}
// بررسی اعتبار کسب و کار
$business = $acc['bid'];
$currentBalance = (float) ($business->getSmsCharge() ?? 0);
// محاسبه هزینه تخمینی (حداقل 100 ریال)
$estimatedCost = 100;
if ($currentBalance < $estimatedCost) {
return $this->json([
'success' => false,
'error' => "اعتبار شما کافی نیست (اعتبار فعلی: {$currentBalance} ریال). برای شارژ حساب خود به بخش شارژ مراجعه کنید.",
'balance' => $currentBalance,
'required' => $estimatedCost,
'showChargeButton' => true
'showChargeButton' => true,
'debug_info' => [
'balance' => $currentBalance,
'required' => $estimatedCost
]
]);
}
// استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست
$result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId);
$result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId, $acc);
if ($result['success']) {
// بررسی وجود کلید response
$responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد';
$response = [
@ -132,7 +147,7 @@ class wizardController extends AbstractController
return $this->json([
'success' => false,
'error' => $result['error'] ?? 'خطای نامشخص در سرویس هوش مصنوعی',
'debug_info' => $result['debug_info'] ?? null
'debug_info' => $result['debug_info'] ?? ['fallback' => 'no debug info from service', 'result' => $result]
]);
}
@ -184,112 +199,5 @@ class wizardController extends AbstractController
}
}
#[Route('/api/wizard/models', name: 'wizard_models', methods: ['GET'])]
public function wizard_models(): JsonResponse
{
try {
$agentSource = $this->agiService->getAIAgentSource();
$currentModel = $this->agiService->getAIModel();
// لیست مدل‌های موجود بر اساس منبع ایجنت
$models = [];
switch ($agentSource) {
case 'gapgpt':
$models = [
'gpt-4o' => 'مدل پیشرفته',
'gpt-4-turbo' => 'مدل سریع',
'gpt-3.5-turbo' => 'مدل استاندارد',
'claude-3-opus' => 'مدل تحلیلی',
'claude-3-sonnet' => 'مدل متعادل',
'gemini-pro' => 'مدل چندمنظوره'
];
break;
case 'avalai':
$models = [
'gpt-4' => 'مدل پیشرفته',
'gpt-3.5-turbo' => 'مدل استاندارد',
'claude-3' => 'مدل تحلیلی',
'gemini-pro' => 'مدل چندمنظوره'
];
break;
case 'local':
$models = [
'local-model' => 'مدل محلی',
'custom-model' => 'مدل سفارشی'
];
break;
}
return $this->json([
'success' => true,
'models' => $models,
'current_model' => $currentModel,
'service_name' => $this->agiService->getServiceDisplayName($agentSource)
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => 'خطا در دریافت مدل‌ها: ' . $e->getMessage()
]);
}
}
#[Route('/api/wizard/settings', name: 'wizard_settings', methods: ['GET'])]
public function wizard_settings(): JsonResponse
{
try {
$agentSource = $this->agiService->getAIAgentSource();
return $this->json([
'success' => true,
'settings' => [
'aiEnabled' => $this->agiService->isAIEnabled(),
'serviceName' => $this->agiService->getServiceDisplayName($agentSource),
'aiModel' => $this->agiService->getAIModel(),
'inputTokenPrice' => $this->agiService->getInputTokenPrice(),
'outputTokenPrice' => $this->agiService->getOutputTokenPrice(),
'aiPrompt' => $this->agiService->getAIPrompt()
]
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => 'خطا در دریافت تنظیمات: ' . $e->getMessage()
]);
}
}
#[Route('/api/wizard/balance', name: 'wizard_balance', methods: ['GET'])]
public function wizard_balance(Access $access): JsonResponse
{
try {
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// بررسی دسترسی هوش مصنوعی
if (!$acc['ai']) {
return $this->json([
'success' => false,
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید'
]);
}
$business = $acc['bid'];
$balance = (float) ($business->getSmsCharge() ?? 0);
return $this->json([
'success' => true,
'balance' => $balance,
'formatted_balance' => number_format($balance, 0, '.', ',') . ' ریال'
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'error' => 'خطا در دریافت اعتبار: ' . $e->getMessage()
]);
}
}
}

View file

@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\AIMessageRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: AIMessageRepository::class)]
class AIMessage
@ -16,6 +17,7 @@ class AIMessage
#[ORM\ManyToOne(inversedBy: 'messages')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?AIConversation $conversation = null;
#[ORM\Column(length: 20)]

View file

@ -6,6 +6,7 @@ use App\Repository\InvoiceTypeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: InvoiceTypeRepository::class)]
class InvoiceType
@ -28,12 +29,14 @@ class InvoiceType
private ?string $type = null;
#[ORM\OneToMany(mappedBy: 'InvoiceLabel', targetEntity: HesabdariDoc::class)]
#[Ignore]
private Collection $hesabdariDocs;
/**
* @var Collection<int, PreInvoiceDoc>
*/
#[ORM\OneToMany(mappedBy: 'invoiceLabel', targetEntity: PreInvoiceDoc::class)]
#[Ignore]
private Collection $preInvoiceDocs;
public function __construct()

View file

@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugGhestaDocRepository::class)]
class PlugGhestaDoc
@ -18,9 +19,11 @@ class PlugGhestaDoc
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Business $bid = null;
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[Ignore]
private ?User $submitter = null;
#[ORM\Column(length: 25)]
@ -43,16 +46,19 @@ class PlugGhestaDoc
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Person $person = null;
/**
* @var Collection<int, PlugGhestaItem>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaItem::class, mappedBy: 'doc', orphanRemoval: true)]
#[Ignore]
private Collection $plugGhestaItems;
#[ORM\ManyToOne(inversedBy: 'plugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?HesabdariDoc $mainDoc = null;
public function __construct()

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\PlugGhestaItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugGhestaItemRepository::class)]
class PlugGhestaItem
@ -15,6 +16,7 @@ class PlugGhestaItem
#[ORM\ManyToOne(inversedBy: 'plugGhestaItems')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PlugGhestaDoc $doc = null;
#[ORM\Column(length: 25)]
@ -27,6 +29,7 @@ class PlugGhestaItem
private ?int $num = null;
#[ORM\ManyToOne(inversedBy: 'plugGhestaItems')]
#[Ignore]
private ?HesabdariDoc $hesabdariDoc = null;
public function getId(): ?int

View file

@ -6,6 +6,7 @@ use App\Repository\PlugHrmDocRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugHrmDocRepository::class)]
class PlugHrmDoc
@ -23,19 +24,23 @@ class PlugHrmDoc
#[ORM\ManyToOne(inversedBy: 'plugHrmDocs')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Business $business = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?User $creator = null;
#[ORM\Column]
private ?int $createDate = null;
#[ORM\OneToMany(mappedBy: 'doc', targetEntity: PlugHrmDocItem::class, orphanRemoval: true)]
#[Ignore]
private Collection $items;
#[ORM\ManyToOne(inversedBy: 'plugHrmDocs')]
#[Ignore]
private ?HesabdariDoc $hesabdariDoc = null;
public function __construct()

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\PlugHrmDocItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PlugHrmDocItemRepository::class)]
class PlugHrmDocItem
@ -15,10 +16,12 @@ class PlugHrmDocItem
#[ORM\ManyToOne(inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PlugHrmDoc $doc = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Person $person = null;
#[ORM\Column]

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use App\Repository\PreInvoiceItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore;
#[ORM\Entity(repositoryClass: PreInvoiceItemRepository::class)]
class PreInvoiceItem
@ -15,6 +16,7 @@ class PreInvoiceItem
#[ORM\ManyToOne(inversedBy: 'preInvoiceItems')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?Commodity $commodity = null;
#[ORM\Column(length: 100, nullable: true)]
@ -46,6 +48,7 @@ class PreInvoiceItem
#[ORM\ManyToOne(inversedBy: 'preInvoiceItems')]
#[ORM\JoinColumn(nullable: false)]
#[Ignore]
private ?PreInvoiceDoc $doc = null;
public function getId(): ?int

View file

@ -10,6 +10,9 @@ use App\Service\Log;
use App\Service\Provider;
use App\Service\AGI\Promps\PromptService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
class AGIService
{
@ -18,19 +21,25 @@ class AGIService
private $log;
private $provider;
private $promptService;
private $httpClient;
private $httpKernel;
public function __construct(
EntityManagerInterface $entityManager,
registryMGR $registryMGR,
Log $log,
Provider $provider,
PromptService $promptService
PromptService $promptService,
HttpClientInterface $httpClient,
HttpKernelInterface $httpKernel
) {
$this->em = $entityManager;
$this->registryMGR = $registryMGR;
$this->log = $log;
$this->provider = $provider;
$this->promptService = $promptService;
$this->httpClient = $httpClient;
$this->httpKernel = $httpKernel;
}
/**
@ -39,9 +48,10 @@ class AGIService
* @param Business|null $business کسب و کار
* @param mixed $user کاربر
* @param int|null $conversationId شناسه گفتگو (اختیاری)
* @param array|null $acc اطلاعات دسترسی
* @return array
*/
public function sendRequest(string $message, ?Business $business = null, $user = null, ?int $conversationId = null): array
public function sendRequest(string $message, ?Business $business = null, $user = null, ?int $conversationId = null, ?array $acc = null): array
{
// بررسی فعال بودن هوش مصنوعی
$status = $this->checkAIServiceStatus();
@ -49,7 +59,16 @@ class AGIService
return [
'success' => false,
'error' => 'سرویس هوش مصنوعی غیرفعال است.',
'status' => $status
'debug_info' => [
'context' => 'sendRequest',
'service_status' => $status,
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
@ -69,76 +88,80 @@ class AGIService
if (!$apiKey) {
return [
'success' => false,
'error' => 'کلید API برای سرویس هوش مصنوعی تنظیم نشده است.'
'error' => 'کلید API برای سرویس هوش مصنوعی تنظیم نشده است.',
'debug_info' => [
'context' => 'sendRequest',
'service' => $service,
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
// ارسال درخواست به سرویس هوش مصنوعی
$response = $this->sendToAIService($prompt, $apiKey, $service, $conversationHistory);
// ارسال درخواست با function calling
$result = $this->sendToAIServiceWithFunctionCalling($prompt, $apiKey, $service, $conversationHistory, $acc);
if ($response['success']) {
// پردازش پاسخ
$aiResponse = $this->extractAIResponse($response['data']);
$cost = $this->calculateCostFromResponse($response['data']);
if (!$result['success']) {
return $result;
}
// استخراج پاسخ نهایی
$aiResponse = $this->extractAIResponse($result['data']);
$cost = $this->calculateCostFromResponse($result['data']);
// ذخیره پاسخ هوش مصنوعی
$this->saveAIMessage($conversation, $aiResponse, $response['data'], $cost);
$this->saveAIMessage($conversation, $aiResponse, $result['data'], $cost);
return [
'success' => true,
'response' => $aiResponse,
'usage' => $response['data']['usage'] ?? null,
'conversationId' => $conversation->getId(),
'model' => $this->getAIModel(),
'usage' => $result['data']['usage'] ?? [],
'cost' => $cost,
'service' => $service,
'model' => $this->getAIModel(),
'conversation_id' => $conversation->getId()
];
}
return $response;
'debug_info' => [
'context' => 'sendRequest',
'function_calls' => $result['function_calls'] ?? [],
'tool_results' => $result['tool_results'] ?? []
]
];
} catch (\Exception $e) {
$this->log->error('خطا در ارسال درخواست به هوش مصنوعی: ' . $e->getMessage(), [
'context' => 'AGIService::sendRequest',
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'error' => 'خطا در پردازش درخواست: ' . $e->getMessage()
'error' => 'خطا در پردازش درخواست: ' . $e->getMessage(),
'debug_info' => [
'context' => 'sendRequest',
'exception' => $e->getMessage(),
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
}
/**
* ساخت پرامپ هوشمند
* ارسال درخواست به سرویس هوش مصنوعی با پشتیبانی از function calling
*/
private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string
{
// دریافت پرامپ‌های پایه
$basePrompts = $this->promptService->getAllPromptsAsString();
$prompt = $basePrompts;
// اضافه کردن اطلاعات کسب و کار
if ($business) {
$prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}.";
}
// اضافه کردن تاریخچه گفتگو
if (!empty($conversationHistory)) {
$prompt .= "\n\n📜 تاریخچه گفتگو:\n";
foreach ($conversationHistory as $historyItem) {
$role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار';
$prompt .= "{$role}: {$historyItem['content']}\n";
}
$prompt .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید.";
}
$prompt .= "\n\nسوال کاربر: " . $message;
return $prompt;
}
/**
* ارسال درخواست به سرویس هوش مصنوعی
*/
private function sendToAIService(string $prompt, string $apiKey, string $service, array $conversationHistory = []): array
private function sendToAIServiceWithFunctionCalling(string $prompt, string $apiKey, string $service, array $conversationHistory = [], ?array $acc = null): array
{
$urls = $this->getServiceUrls($service);
$model = $this->getAIModel();
@ -160,27 +183,222 @@ class AGIService
'content' => $prompt
];
// تعریف ابزارهای موجود
$tools = $this->buildToolsFromPromptServices();
$data = [
'model' => $model,
'messages' => $messages,
'tools' => $tools,
'tool_choice' => 'auto', // اجازه انتخاب ابزار به مدل
'max_tokens' => 12000,
'temperature' => 0.1
];
// تلاش برای ارسال به URL های مختلف
foreach ($urls as $url) {
$result = $this->makeHttpRequest($url, $data, $apiKey);
if ($result['success']) {
return $result;
$maxIterations = 5; // حداکثر تعداد تکرار برای جلوگیری از حلقه بی‌نهایت
$iteration = 0;
$functionCalls = [];
$toolResults = [];
while ($iteration < $maxIterations) {
$iteration++;
// تلاش برای ارسال به URL های مختلف
$result = null;
foreach ($urls as $url) {
$result = $this->makeHttpRequest($url, $data, $apiKey);
if ($result['success']) {
break;
}
}
if (!$result || !$result['success']) {
return [
'success' => false,
'error' => 'خطا در ارتباط با سرور هوش مصنوعی.',
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'url_list' => $urls,
'data' => $data,
'iteration' => $iteration
]
];
}
$responseData = $result['data'];
$choices = $responseData['choices'] ?? [];
if (empty($choices)) {
return [
'success' => false,
'error' => 'پاسخ نامعتبر از سرور هوش مصنوعی.',
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'response_data' => $responseData,
'iteration' => $iteration
]
];
}
$choice = $choices[0];
$message = $choice['message'] ?? [];
$toolCalls = $message['tool_calls'] ?? [];
// اگر ابزاری فراخوانی نشده، پاسخ نهایی است
if (empty($toolCalls)) {
return [
'success' => true,
'data' => $responseData,
'function_calls' => $functionCalls,
'tool_results' => $toolResults
];
}
// اجرای ابزارهای فراخوانی شده
$toolResults = [];
foreach ($toolCalls as $toolCall) {
$functionCall = $toolCall['function'] ?? [];
$functionName = $functionCall['name'] ?? '';
$functionArgs = json_decode($functionCall['arguments'] ?? '{}', true);
if ($acc && !isset($functionArgs['acc'])) {
$functionArgs['acc'] = $acc;
}
$functionCalls[] = [
'name' => $functionName,
'arguments' => $functionArgs
];
// اجرای ابزار
$toolResult = $this->callTool($functionName, $functionArgs);
$toolResults[] = [
'tool_call_id' => $toolCall['id'] ?? '',
'role' => 'tool',
'content' => json_encode($toolResult, JSON_UNESCAPED_UNICODE)
];
}
// اضافه کردن نتایج ابزارها به messages برای ارسال مجدد
$messages[] = [
'role' => 'assistant',
'content' => null,
'tool_calls' => $toolCalls
];
foreach ($toolResults as $toolResult) {
$messages[] = $toolResult;
}
// به‌روزرسانی data برای ارسال مجدد
$data['messages'] = $messages;
}
// اگر به حداکثر تعداد تکرار رسیدیم
return [
'success' => false,
'error' => 'خطا در ارتباط با سرور هوش مصنوعی.'
'error' => 'تعداد تکرار بیش از حد مجاز.',
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'max_iterations' => $maxIterations,
'function_calls' => $functionCalls,
'tool_results' => $toolResults
]
];
}
/**
* اجرای ابزار
*/
private function callTool(string $tool, array $params, array $context = [])
{
try {
switch ($tool) {
case 'getPersonInfo':
// استفاده مستقیم از سرویس Cog\PersonService
return $this->callGetPersonInfoFromCog($params);
default:
return [
'error' => 'ابزار ناشناخته: ' . $tool
];
}
} catch (\Exception $e) {
$this->log->error('خطا در اجرای ابزار: ' . $e->getMessage(), [
'context' => 'AGIService::callTool',
'tool' => $tool,
'params' => $params,
'exception' => $e->getMessage()
]);
return [
'error' => 'خطا در اجرای ابزار: ' . $e->getMessage()
];
}
}
/**
* اجرای ابزار getPersonInfo با استفاده از سرویس Cog\PersonService
*/
private function callGetPersonInfoFromCog(array $params)
{
$code = $params['code'] ?? null;
if (!$code) {
return [
'error' => 'کد شخص الزامی است'
];
}
try {
// دریافت اطلاعات دسترسی (acc) از context یا پارامترها
$acc = $params['acc'] ?? null;
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
// استفاده از سرویس Cog\PersonService
$personService = new \App\Cog\PersonService($this->em, $this->provider->getAccessService());
$result = $personService->getPersonInfo($code, $acc);
return $result;
} catch (\Exception $e) {
$this->log->error('خطا در دریافت اطلاعات شخص از Cog: ' . $e->getMessage(), [
'context' => 'AGIService::callGetPersonInfoFromCog',
'code' => $code,
'exception' => $e->getMessage()
]);
return [
'error' => 'خطا در دریافت اطلاعات شخص: ' . $e->getMessage()
];
}
}
/**
* ساخت پرامپ هوشمند
*/
private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string
{
// دریافت پرامپ‌های پایه از PromptService
$basePrompts = $this->promptService->getAllBasePrompts();
$prompt = $basePrompts;
// قوانین خروجی JSON و مثال‌ها از سرویس مدیریت پرامپت‌ها
$prompt .= $this->promptService->getOutputFormatPrompt();
// اضافه کردن اطلاعات کسب و کار
if ($business) {
$prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}.";
}
// اضافه کردن تاریخچه گفتگو
if (!empty($conversationHistory)) {
$prompt .= "\n\n📜 تاریخچه گفتگو:\n";
foreach ($conversationHistory as $historyItem) {
$role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار';
$prompt .= "{$role}: {$historyItem['content']}\n";
}
$prompt .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید.";
}
$prompt .= "\n\nسوال کاربر: " . $message;
return $prompt;
}
/**
* دریافت URL های سرویس
*/
@ -188,10 +406,10 @@ class AGIService
{
return match ($service) {
'gapgpt' => [
'https://api.gapgpt.app/v1/chat/completions',
'https://api.gapgpt.ir/v1/chat/completions'
'https://api.gapgpt.ir/v1/chat/completions',
'https://api.gapgpt.app/v1/chat/completions'
],
'avalai' => ['https://api.avalai.com/v1/chat/completions'],
'avalai' => ['https://api.avalapis.ir/v1/chat/completions'],
'local' => [$this->registryMGR->get('system', 'localModelAddress') ?? ''],
default => []
};
@ -202,59 +420,62 @@ class AGIService
*/
private function makeHttpRequest(string $url, array $data, string $apiKey): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
],
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_USERAGENT => 'Hesabix-AGI-Service/1.0'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return [
'success' => false,
'error' => 'خطا در ارتباط با سرور: ' . $error
];
}
if ($httpCode !== 200) {
return [
'success' => false,
'error' => 'خطای HTTP: ' . $this->getHttpErrorMessage($httpCode)
];
}
$responseData = json_decode($response, true);
if (!$responseData) {
return [
'success' => false,
'error' => 'پاسخ نامعتبر از سرور: ' . substr($response, 0, 200)
];
}
return [
'success' => true,
'data' => $responseData
$debugInfo = [
'request_url' => $url,
'request_data' => $data,
];
try {
$response = $this->httpClient->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $apiKey,
],
'json' => $data,
'timeout' => 15,
'max_redirects' => 3,
]);
$statusCode = $response->getStatusCode();
$content = $response->getContent(false); // false: throw exception on 4xx/5xx نمی‌دهد
$debugInfo['http_code'] = $statusCode;
$debugInfo['raw_response'] = $content;
if ($statusCode !== 200) {
$debugInfo['http_error_message'] = $this->getHttpErrorMessage($statusCode);
return [
'success' => false,
'error' => 'خطای HTTP: ' . $this->getHttpErrorMessage($statusCode),
'debug_info' => $debugInfo
];
}
$responseData = json_decode($content, true);
if (!$responseData) {
$debugInfo['json_error'] = json_last_error_msg();
return [
'success' => false,
'error' => 'پاسخ نامعتبر از سرور: ' . substr($content, 0, 200),
'debug_info' => $debugInfo
];
}
return [
'success' => true,
'data' => $responseData,
'debug_info' => $debugInfo
];
} catch (\Throwable $e) {
$debugInfo['exception'] = $e->getMessage();
$debugInfo['exception_code'] = $e->getCode();
$debugInfo['exception_trace'] = $e->getTraceAsString();
return [
'success' => false,
'error' => 'خطا در ارتباط با سرور: ' . $e->getMessage(),
'debug_info' => $debugInfo
];
}
}
/**
@ -606,4 +827,12 @@ class AGIService
return $result;
}
/**
* ساخت ابزارها از سرویس‌های پرامپ
*/
private function buildToolsFromPromptServices(): array
{
return $this->promptService->getAllTools();
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class BankPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش بانک‌ها برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getBankAccountInfo
$bankAccountInfoPrompt = $this->getBankAccountInfoPrompt();
$bankAccountInfoData = json_decode($bankAccountInfoPrompt, true);
if ($bankAccountInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $bankAccountInfoData['tool'],
'description' => $bankAccountInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $bankAccountInfoData['input'],
'required' => array_keys($bankAccountInfoData['input'])
]
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش بانک‌ها
* @return string
*/
public function getAllBankPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getBankAccountInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreateBankAccountPrompt();
// $prompts[] = $this->getUpdateBankAccountPrompt();
// $prompts[] = $this->getBankTransactionPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
}
/**
* پرامپ برای دریافت اطلاعات کامل حساب بانکی
* @return string
*/
public function getBankAccountInfoPrompt(): string
{
return '{
"tool": "getBankAccountInfo",
"description": "Get complete bank account information by code",
"endpoint": "/api/bank/account/info/{code}",
"method": "GET",
"input": {
"code": "string - Bank account code (e.g., 1001, 1002)"
},
"output": {
"id": "integer - Account ID",
"code": "string - Account code",
"name": "string - Account name",
"bankName": "string - Bank name",
"accountNumber": "string - Account number",
"shabaNumber": "string - Shaba number",
"cardNumber": "string - Card number",
"balance": "float - Current balance",
"currency": "string - Currency type",
"isActive": "boolean - Account active status",
"description": "string - Account description"
},
"examples": {
"input": {"code": "1001"},
"output": {
"id": 23,
"code": "1001",
"name": "حساب جاری اصلی",
"bankName": "ملت",
"accountNumber": "1234567890",
"shabaNumber": "IR123456789012345678901234",
"cardNumber": "6104337812345678",
"balance": 150000000,
"currency": "ریال",
"isActive": true,
"description": "حساب جاری اصلی شرکت"
}
}
}';
}
}

View file

@ -3,14 +3,18 @@
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\Access;
use App\Entity\APIToken;
class BasePromptService
{
private $em;
private $access;
public function __construct(EntityManagerInterface $entityManager)
public function __construct(EntityManagerInterface $entityManager, Access $access)
{
$this->em = $entityManager;
$this->access = $access;
}
/**
@ -19,6 +23,29 @@ class BasePromptService
*/
public function getSystemIntroductionPrompt(): string
{
// دسترسی فعلی
$acc = $this->access->hasRole('join');
$apiToken = null;
if ($acc && isset($acc['bid']) && isset($acc['user'])) {
// جستجوی توکن AI معتبر برای این کاربر و کسب‌وکار
$now = time();
$apiToken = $this->em->getRepository(APIToken::class)->findOneBy([
'bid' => $acc['bid'],
'submitter' => $acc['user'],
'isForAi' => true
]);
if ($apiToken) {
$expire = $apiToken->getDateExpire();
if ($expire && $expire != '0' && $now > (int)$expire) {
$apiToken = null; // منقضی شده
}
}
if (!$apiToken) {
// ساخت توکن جدید با اعتبار ۳۰ دقیقه
$apiToken = $this->access->createAiToken($acc['bid'], $acc['user'], 1800);
}
}
$apiKey = $apiToken ? $apiToken->getToken() : '';
return '{
"tool": "system_introduction",
"description": "System introduction and authentication requirements",
@ -29,7 +56,7 @@ class BasePromptService
"authentication": {
"method": "API Key or Session Token",
"required_headers": {
"api-key": "API token for AI access (required for AI operations)",
"api-key": "' . $apiKey . ' (این کد را در هدر api-key قرار بده)",
},
},
"language": "Persian (فارسی)",
@ -87,6 +114,24 @@ class BasePromptService
}';
}
/**
* پرامپ برای نمایش دامنه اصلی API
* @return string
*/
public function getApiBaseUrlPrompt(): string
{
// دریافت اولین رکورد تنظیمات
$settings = $this->em->getRepository(\App\Entity\Settings::class)->findAll();
$appSite = isset($settings[0]) ? $settings[0]->getAppSite() : '';
$domain = $appSite ? $appSite : '---';
return '{
"tool": "api_base_url",
"description": "آدرس پایه API",
"content": "تمام اندپوینت‌های سیستم از طریق دامنه زیر قابل دسترسی هستند:",
"base_url": "' . $domain . '"
}';
}
/**
* دریافت تمام پرامپ‌های پایه
* @return string
@ -98,6 +143,7 @@ class BasePromptService
$prompts[] = $this->getSystemIntroductionPrompt();
$prompts[] = $this->getErrorHandlingPrompt();
$prompts[] = $this->getHelpPrompt();
$prompts[] = $this->getApiBaseUrlPrompt();
return implode("\n\n", $prompts);
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class InventoryPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش کالاها برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getItemInfo
$itemInfoPrompt = $this->getItemInfoPrompt();
$itemInfoData = json_decode($itemInfoPrompt, true);
if ($itemInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $itemInfoData['tool'],
'description' => $itemInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $itemInfoData['input'],
'required' => array_keys($itemInfoData['input'])
]
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش کالاها
* @return string
*/
public function getAllInventoryPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getItemInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreateItemPrompt();
// $prompts[] = $this->getUpdateItemPrompt();
// $prompts[] = $this->getSearchItemPrompt();
// $prompts[] = $this->getItemStockPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
}
/**
* پرامپ برای دریافت اطلاعات کامل کالا
* @return string
*/
public function getItemInfoPrompt(): string
{
return '{
"tool": "getItemInfo",
"description": "Get complete item information by code",
"endpoint": "/api/item/info/{code}",
"method": "GET",
"input": {
"code": "string - Item code (e.g., 1001, 1002)"
},
"output": {
"id": "integer - Item ID",
"code": "string - Item code",
"name": "string - Item name",
"description": "string - Item description",
"category": "string - Item category",
"unit": "string - Unit of measurement",
"price": "float - Item price",
"stock": "float - Current stock quantity",
"minStock": "float - Minimum stock level",
"maxStock": "float - Maximum stock level",
"supplier": "string - Supplier name",
"barcode": "string - Barcode",
"isActive": "boolean - Item active status"
},
"examples": {
"input": {"code": "1001"},
"output": {
"id": 45,
"code": "1001",
"name": "لپ‌تاپ اپل",
"description": "لپ‌تاپ اپل مک‌بوک پرو 13 اینچ",
"category": "الکترونیک",
"unit": "عدد",
"price": 45000000,
"stock": 15,
"minStock": 5,
"maxStock": 50,
"supplier": "شرکت اپل",
"barcode": "1234567890123",
"isActive": true
}
}
}';
}
}

View file

@ -13,6 +13,36 @@ class PersonPromptService
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش اشخاص برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getPersonInfo
$personInfoPrompt = $this->getPersonInfoPrompt();
$personInfoData = json_decode($personInfoPrompt, true);
if ($personInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $personInfoData['tool'],
'description' => $personInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $personInfoData['input'],
'required' => array_keys($personInfoData['input'])
]
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش اشخاص
* @return string

View file

@ -3,16 +3,65 @@
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\AGI\Promps\InventoryPromptService;
use App\Service\AGI\Promps\BankPromptService;
class PromptService
{
private $em;
private $personPromptService;
private $basePromptService;
private $inventoryPromptService;
private $bankPromptService;
public function __construct(EntityManagerInterface $entityManager, PersonPromptService $personPromptService)
{
public function __construct(
EntityManagerInterface $entityManager,
PersonPromptService $personPromptService,
BasePromptService $basePromptService,
InventoryPromptService $inventoryPromptService,
BankPromptService $bankPromptService
) {
$this->em = $entityManager;
$this->personPromptService = $personPromptService;
$this->basePromptService = $basePromptService;
$this->inventoryPromptService = $inventoryPromptService;
$this->bankPromptService = $bankPromptService;
}
/**
* دریافت تمام ابزارهای موجود برای function calling
* @return array
*/
public function getAllTools(): array
{
$tools = [];
// ابزارهای بخش اشخاص
$personTools = $this->personPromptService->getTools();
$tools = array_merge($tools, $personTools);
// ابزارهای بخش کالاها
$inventoryTools = $this->inventoryPromptService->getTools();
$tools = array_merge($tools, $inventoryTools);
// ابزارهای بخش بانک‌ها
$bankTools = $this->bankPromptService->getTools();
$tools = array_merge($tools, $bankTools);
// در آینده ابزارهای بخش‌های دیگر اضافه خواهند شد
// $accountingTools = $this->accountingPromptService->getTools();
// $tools = array_merge($tools, $accountingTools);
return $tools;
}
/**
* دریافت تمام پرامپ‌های پایه (سیستم، API، و غیره)
* @return string
*/
public function getAllBasePrompts(): string
{
return $this->basePromptService->getAllBasePrompts();
}
/**
@ -32,6 +81,8 @@ class PromptService
// return $this->inventoryPromptService->getAllInventoryPrompts();
// case 'reports':
// return $this->reportsPromptService->getAllReportsPrompts();
// case 'bank':
// return $this->bankPromptService->getAllBankPrompts();
default:
return null;
}
@ -45,15 +96,24 @@ class PromptService
{
$prompts = [];
// پرامپ‌های پایه (سیستم، API، و غیره)
$prompts['base'] = $this->basePromptService->getAllBasePrompts();
// پرامپ‌های بخش اشخاص
$prompts['person'] = $this->personPromptService->getAllPersonPrompts();
// پرامپ‌های بخش کالاها
$prompts['inventory'] = $this->inventoryPromptService->getAllInventoryPrompts();
// پرامپ‌های بخش بانک‌ها
$prompts['bank'] = $this->bankPromptService->getAllBankPrompts();
// در آینده بخش‌های دیگر اضافه خواهند شد
// $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts();
// $prompts['inventory'] = $this->inventoryPromptService->getAllInventoryPrompts();
// $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts();
// $prompts['sales'] = $this->salesPromptService->getAllSalesPrompts();
// $prompts['purchases'] = $this->purchasesPromptService->getAllPurchasesPrompts();
// $prompts['bank'] = $this->bankPromptService->getAllBankPrompts();
return $prompts;
}
@ -67,4 +127,41 @@ class PromptService
$allPrompts = $this->getAllSystemPrompts();
return implode("\n\n", $allPrompts);
}
/**
* قوانین خروجی JSON و مثال برای پاسخ هوش مصنوعی
* @return string
*/
public function getOutputFormatPrompt(): string
{
$prompt = "\n\nقوانین مهم: همیشه پاسخ را فقط در قالب یک شیء JSON با ساختار زیر ارسال کن و هیچ توضیح اضافه‌ای ننویس:\n";
$prompt .= <<<EOT
{
"type": ["text", "table", "chart"],
"data": [
{ "type": "text", "content": "..." },
{ "type": "table", "headers": [...], "rows": [[...], ...] },
{ "type": "chart", "chartType": "bar|line|pie", "title": "عنوان نمودار", "labels": ["برچسب۱", "برچسب۲"], "series": [{"name": "عنوان سری", "data": [100, 200]}], "values": [100, 200] }
]
}
EOT;
$prompt .= "\nدر صورت نیاز می‌توانی چند نوع داده را همزمان ارسال کنی. اگر فقط متن است، فقط یک آبجکت با type=text و content بده.\n\nمثال خروجی صحیح برای نمودار (دقیقاً همین ساختار را رعایت کن و کلید type را فقط یک بار و مقدار آن را 'chart' قرار بده. نوع نمودار فقط در chartType باشد):\n";
$prompt .= <<<EOT2
{
"type": ["chart"],
"data": [
{
"type": "chart",
"chartType": "bar",
"title": "نمودار فروش ماهانه",
"labels": ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد"],
"series": [{"name": "فروش ماهانه", "data": [120, 150, 180, 200, 170]}],
"values": [120, 150, 180, 200, 170]
}
]
}
EOT2;
$prompt .= "\nحتماً کلیدهای type، title، labels، series، values و chartType را دقیقاً مطابق مثال بالا برای هر نمودار برگردان و فقط همین JSON را خروجی بده. کلید type را فقط یک بار و مقدار آن را 'chart' قرار بده و نوع نمودار را فقط در chartType بنویس.";
return $prompt;
}
}

View file

@ -17,25 +17,20 @@
@click="refreshChart"
:title="$t('chart.refresh')"
></v-btn>
<v-btn
icon="mdi-fullscreen"
variant="text"
size="small"
@click="toggleFullscreen"
:title="$t('chart.fullscreen')"
></v-btn>
</div>
</v-card-title>
<v-card-text class="pa-4">
<div class="chart-container" :class="{ 'fullscreen': isFullscreen }">
<div class="chart-container">
<apexchart
v-if="chartSeries && chartSeries.length > 0 && chartOptions && chartOptions.xaxis && Array.isArray(chartOptions.xaxis.categories)"
ref="chart"
:type="chartType"
:height="chartHeight"
:options="chartOptions"
:series="chartSeries"
></apexchart>
/>
<div v-else class="text-center pa-4" style="color: #888;">دادهای برای نمایش نمودار وجود ندارد.</div>
</div>
<!-- اطلاعات نمودار -->
@ -89,7 +84,6 @@ export default {
},
data() {
return {
isFullscreen: false,
chartId: '',
createdAt: new Date(),
chartType: 'bar',
@ -100,9 +94,6 @@ export default {
},
computed: {
chartHeight() {
if (this.isFullscreen) {
return window.innerHeight - 200;
}
return this.height;
},
dataPointsCount() {
@ -124,6 +115,7 @@ export default {
watch: {
chartData: {
handler(newData) {
console.debug('AIChart.vue watch chartData', newData);
this.initializeChart(newData);
},
immediate: true,
@ -131,13 +123,15 @@ export default {
}
},
mounted() {
console.debug('AIChart.vue mounted', this.chartData);
this.initializeChart(this.chartData);
},
methods: {
initializeChart(data) {
console.debug('AIChart.vue initializeChart data:', data);
if (!data) return;
this.chartType = data.type || 'bar';
this.chartType = data.chartType || 'bar'; // اصلاح مقداردهی نوع نمودار
this.chartTitle = data.title || 'نمودار';
this.chartId = data.chart_id || this.generateChartId();
this.createdAt = new Date();
@ -202,7 +196,7 @@ export default {
},
y: {
formatter: function(value) {
return this.$filters ? this.$filters.formatNumber(value) : value;
return typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
},
@ -218,10 +212,10 @@ export default {
};
// تنظیمات خاص بر اساس نوع نمودار
this.setupChartSpecificOptions(data.data);
this.setupChartSpecificOptions(data);
// تنظیم سریهای داده
this.setupChartSeries(data.data);
this.setupChartSeries(data);
},
setupChartSpecificOptions(data) {
@ -247,7 +241,7 @@ export default {
fontFamily: "'Vazirmatn FD', Arial, sans-serif"
},
formatter: function(value) {
return this.$filters ? this.$filters.formatNumber(value) : value;
return typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
};
@ -273,7 +267,7 @@ export default {
fontSize: '16px',
fontFamily: "'Vazirmatn FD', Arial, sans-serif",
formatter: function(value) {
return this.$filters ? this.$filters.formatNumber(value) : value;
return typeof value === 'number' ? value.toLocaleString('fa-IR') : value;
}
}
}
@ -315,15 +309,55 @@ export default {
},
setupChartSeries(data) {
if (!data) {
this.chartSeries = [];
// مقداردهی پیشفرض به xaxis برای جلوگیری از خطا
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = { categories: [] };
return;
}
if (this.chartType === 'pie' || this.chartType === 'doughnut') {
this.chartSeries = data.series || [];
if (data.values && Array.isArray(data.values)) {
this.chartSeries = data.values;
} else if (
data.series &&
Array.isArray(data.series) &&
data.series.length > 0 &&
Array.isArray(data.series[0].data)
) {
this.chartSeries = data.series[0].data;
} else {
this.chartSeries = [];
}
} else {
this.chartSeries = data.series || [
{
name: 'داده‌ها',
data: []
if (data.series && Array.isArray(data.series) && data.series.length > 0) {
this.chartSeries = data.series;
// مقداردهی categories اگر وجود دارد
if (data.labels && Array.isArray(data.labels)) {
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = this.chartOptions.xaxis || {};
this.chartOptions.xaxis.categories = data.labels;
}
];
} else if (data.labels && data.values && Array.isArray(data.labels) && Array.isArray(data.values)) {
this.chartSeries = [
{
name: this.chartTitle || 'داده‌ها',
data: data.values
}
];
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = this.chartOptions.xaxis || {};
this.chartOptions.xaxis.categories = data.labels;
} else {
this.chartSeries = [
{
name: 'داده‌ها',
data: []
}
];
this.chartOptions = this.chartOptions || {};
this.chartOptions.xaxis = { categories: [] };
}
}
},
@ -356,24 +390,21 @@ export default {
},
downloadChart() {
if (this.$refs.chart) {
this.$refs.chart.chart.downloadCSV();
if (this.$refs.chart && this.$refs.chart.dataURI) {
this.$refs.chart.dataURI().then(({ imgURI }) => {
const link = document.createElement('a');
link.href = imgURI;
link.download = 'chart.png';
link.click();
});
}
},
refreshChart() {
if (this.$refs.chart) {
this.$refs.chart.chart.refresh();
if (this.$refs.chart && this.$refs.chart.updateSeries) {
// داده فعلی را دوباره ست میکنیم تا رفرش شود
this.$refs.chart.updateSeries(this.chartSeries);
}
},
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen;
this.$nextTick(() => {
if (this.$refs.chart) {
this.$refs.chart.chart.resize();
}
});
}
}
};
@ -396,11 +427,20 @@ export default {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
width: 100vw !important;
height: 100vh !important;
z-index: 9999;
background: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container.fullscreen .apexcharts-canvas {
width: 100% !important;
height: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
}
.chart-details {

View file

@ -920,5 +920,15 @@ const fa_lang = {
numberinput: {
invalid_number: "فقط عدد انگلیسی مجاز است"
},
chart: {
download: "دانلود نمودار",
refresh: "نوسازی نمودار",
fullscreen: "تمام‌صفحه",
details: "جزئیات نمودار",
type: "نوع نمودار",
id: "شناسه نمودار",
dataPoints: "تعداد نقاط داده",
created: "تاریخ ایجاد"
},
};
export default fa_lang

View file

@ -15,7 +15,28 @@
</div>
<div class="message-content">
<div class="message-bubble">
<!-- تغییر: رندر داینامیک بر اساس نوع داده -->
<template v-if="message.type === 'ai' && message.data">
<div v-for="(item, idx) in message.data.data" :key="idx">
<div v-if="item.type === 'text'">{{ item.content }}</div>
<v-table v-else-if="item.type === 'table'" class="my-2" density="compact">
<thead>
<tr>
<th v-for="h in item.headers" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rIdx) in item.rows" :key="rIdx">
<td v-for="(cell, cIdx) in row" :key="cIdx">{{ cell }}</td>
</tr>
</tbody>
</v-table>
<AIChart v-else-if="item.type === 'chart' || item.chartType" :chartData="item" height="300" class="my-2" />
</div>
</template>
<template v-else>
<p class="message-text">{{ message.text }}</p>
</template>
<span class="message-time">{{ formatTime(message.timestamp) }}</span>
</div>
</div>
@ -96,16 +117,18 @@
<script>
import { useNavigationStore } from '@/stores/navigationStore';
import axios from 'axios';
import AIChart from '@/components/widgets/AIChart.vue';
export default {
name: "WizardHome",
components: { AIChart },
data() {
return {
navigationStore: useNavigationStore(),
messages: [
{
type: 'ai',
text: 'سلام! من دستیار هوشمند شما هستم. چطور می‌تونم کمکتون کنم؟',
data: { type: ['text'], data: [{ type: 'text', content: 'سلام! من دستیار هوشمند شما هستم. چطور می‌تونم کمکتون کنم؟' }] },
timestamp: new Date()
}
],
@ -137,7 +160,7 @@ export default {
// تغییر پیام اولیه بر اساس وضعیت
this.messages[0] = {
type: 'ai',
text: this.getStatusMessage(data.status, data.message),
data: { type: ['text'], data: [{ type: 'text', content: this.getStatusMessage(data.status, data.message) }] },
timestamp: new Date()
};
}
@ -146,7 +169,7 @@ export default {
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
text: 'خطا در بررسی وضعیت هوش مصنوعی. لطفاً دوباره تلاش کنید.',
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در بررسی وضعیت هوش مصنوعی. لطفاً دوباره تلاش کنید.' }] },
timestamp: new Date()
};
}
@ -155,7 +178,7 @@ export default {
this.aiStatus = 'error';
this.messages[0] = {
type: 'ai',
text: 'خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید.',
data: { type: ['text'], data: [{ type: 'text', content: 'خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید.' }] },
timestamp: new Date()
};
}
@ -210,18 +233,48 @@ export default {
this.isTyping = false;
if (response.data.success) {
// اضافه کردن پاسخ AI
// --- تغییر: پردازش پاسخ JSON ---
let aiData = response.data.response;
let parsed = null;
try {
// اگر پاسخ داخل بلاک کد markdown است، فقط بخش json را جدا کن
if (typeof aiData === 'string' && aiData.trim().startsWith('```json')) {
aiData = aiData.replace(/^```json[\r\n]*/i, '').replace(/```$/i, '').trim();
}
// پارس چند مرحلهای تا رسیدن به آبجکت واقعی
parsed = aiData;
let safety = 0;
while (typeof parsed === 'string' && safety < 5) {
parsed = JSON.parse(parsed);
safety++;
}
// اگر باز هم data.data[0] رشته بود، دوباره پارس کن
if (
parsed &&
parsed.data &&
Array.isArray(parsed.data) &&
typeof parsed.data[0] === 'string'
) {
let safety2 = 0;
while (typeof parsed.data[0] === 'string' && safety2 < 5) {
parsed.data[0] = JSON.parse(parsed.data[0]);
safety2++;
}
}
} catch (e) {
// اگر JSON نبود، به صورت متن نمایش بده
parsed = { type: ['text'], data: [{ type: 'text', content: aiData }] };
}
console.debug('home.vue AI message parsed:', parsed);
this.messages.push({
type: 'ai',
text: response.data.response,
data: parsed,
timestamp: new Date()
});
// ذخیره conversationId برای ادامه گفتگو
if (response.data.conversationId) {
this.conversationId = response.data.conversationId;
}
// نمایش اطلاعات هزینه در صورت وجود
if (response.data.cost) {
console.log('هزینه استفاده:', response.data.cost);
@ -230,11 +283,10 @@ export default {
// نمایش خطا
this.messages.push({
type: 'ai',
text: `خطا: ${response.data.error}`,
data: { type: ['text'], data: [{ type: 'text', content: `خطا: ${response.data.error}` }] },
timestamp: new Date()
});
}
// اسکرول به پایین بعد از دریافت پاسخ
setTimeout(() => {
this.scrollToBottom();
@ -242,7 +294,6 @@ export default {
} catch (error) {
this.isTyping = false;
let errorMessage = 'خطا در ارتباط با سرور';
if (error.response) {
if (error.response.data && error.response.data.error) {
@ -255,13 +306,11 @@ export default {
} else if (error.request) {
errorMessage = 'خطا در اتصال به سرور';
}
this.messages.push({
type: 'ai',
text: errorMessage,
data: { type: ['text'], data: [{ type: 'text', content: errorMessage }] },
timestamp: new Date()
});
setTimeout(() => {
this.scrollToBottom();
}, 200);