diff --git a/hesabixCore/config/services.yaml b/hesabixCore/config/services.yaml index 1567675d..af0b248c 100644 --- a/hesabixCore/config/services.yaml +++ b/hesabixCore/config/services.yaml @@ -95,3 +95,8 @@ services: App\Twig\NumberFormatExtension: tags: ['twig.extension'] + + App\Cog\PersonService: + arguments: + $entityManager: '@doctrine.orm.entity_manager' + $access: '@App\Service\Access' diff --git a/hesabixCore/src/Cog/PersonService.php b/hesabixCore/src/Cog/PersonService.php new file mode 100644 index 00000000..cb29b8ec --- /dev/null +++ b/hesabixCore/src/Cog/PersonService.php @@ -0,0 +1,78 @@ +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; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/PersonsController.php b/hesabixCore/src/Controller/PersonsController.php index 633de983..5f699835 100644 --- a/hesabixCore/src/Controller/PersonsController.php +++ b/hesabixCore/src/Controller/PersonsController.php @@ -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); } diff --git a/hesabixCore/src/Controller/wizardController.php b/hesabixCore/src/Controller/wizardController.php index 9e2b806a..1cd2ab8e 100644 --- a/hesabixCore/src/Controller/wizardController.php +++ b/hesabixCore/src/Controller/wizardController.php @@ -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() - ]); - } - } } diff --git a/hesabixCore/src/Entity/AIMessage.php b/hesabixCore/src/Entity/AIMessage.php index 1cbf7dff..055e5cf1 100644 --- a/hesabixCore/src/Entity/AIMessage.php +++ b/hesabixCore/src/Entity/AIMessage.php @@ -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)] diff --git a/hesabixCore/src/Entity/InvoiceType.php b/hesabixCore/src/Entity/InvoiceType.php index 0e553508..6734bade 100644 --- a/hesabixCore/src/Entity/InvoiceType.php +++ b/hesabixCore/src/Entity/InvoiceType.php @@ -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 */ #[ORM\OneToMany(mappedBy: 'invoiceLabel', targetEntity: PreInvoiceDoc::class)] + #[Ignore] private Collection $preInvoiceDocs; public function __construct() diff --git a/hesabixCore/src/Entity/PlugGhestaDoc.php b/hesabixCore/src/Entity/PlugGhestaDoc.php index fa27a762..5101e1b6 100644 --- a/hesabixCore/src/Entity/PlugGhestaDoc.php +++ b/hesabixCore/src/Entity/PlugGhestaDoc.php @@ -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 */ #[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() diff --git a/hesabixCore/src/Entity/PlugGhestaItem.php b/hesabixCore/src/Entity/PlugGhestaItem.php index 38ce692e..01bca933 100644 --- a/hesabixCore/src/Entity/PlugGhestaItem.php +++ b/hesabixCore/src/Entity/PlugGhestaItem.php @@ -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 diff --git a/hesabixCore/src/Entity/PlugHrmDoc.php b/hesabixCore/src/Entity/PlugHrmDoc.php index 9a5a8f2c..5c51d679 100644 --- a/hesabixCore/src/Entity/PlugHrmDoc.php +++ b/hesabixCore/src/Entity/PlugHrmDoc.php @@ -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() diff --git a/hesabixCore/src/Entity/PlugHrmDocItem.php b/hesabixCore/src/Entity/PlugHrmDocItem.php index 1c156233..c6365dc9 100644 --- a/hesabixCore/src/Entity/PlugHrmDocItem.php +++ b/hesabixCore/src/Entity/PlugHrmDocItem.php @@ -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] diff --git a/hesabixCore/src/Entity/PreInvoiceItem.php b/hesabixCore/src/Entity/PreInvoiceItem.php index 3d982b62..1549cd8e 100644 --- a/hesabixCore/src/Entity/PreInvoiceItem.php +++ b/hesabixCore/src/Entity/PreInvoiceItem.php @@ -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 diff --git a/hesabixCore/src/Service/AGI/AGIService.php b/hesabixCore/src/Service/AGI/AGIService.php index d7e9feac..b8a8408b 100644 --- a/hesabixCore/src/Service/AGI/AGIService.php +++ b/hesabixCore/src/Service/AGI/AGIService.php @@ -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(); + } } \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/BankPromptService.php b/hesabixCore/src/Service/AGI/Promps/BankPromptService.php new file mode 100644 index 00000000..793471e5 --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/BankPromptService.php @@ -0,0 +1,111 @@ +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": "حساب جاری اصلی شرکت" + } + } + }'; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/BasePromptService.php b/hesabixCore/src/Service/AGI/Promps/BasePromptService.php index e9280865..a7665ae3 100644 --- a/hesabixCore/src/Service/AGI/Promps/BasePromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/BasePromptService.php @@ -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); } diff --git a/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php b/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php new file mode 100644 index 00000000..64c34991 --- /dev/null +++ b/hesabixCore/src/Service/AGI/Promps/InventoryPromptService.php @@ -0,0 +1,116 @@ +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 + } + } + }'; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php b/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php index 0441c024..771e261a 100644 --- a/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/PersonPromptService.php @@ -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 diff --git a/hesabixCore/src/Service/AGI/Promps/PromptService.php b/hesabixCore/src/Service/AGI/Promps/PromptService.php index dfeaf575..a6117eb1 100644 --- a/hesabixCore/src/Service/AGI/Promps/PromptService.php +++ b/hesabixCore/src/Service/AGI/Promps/PromptService.php @@ -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 .= << - -
+
+ /> +
داده‌ای برای نمایش نمودار وجود ندارد.
@@ -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 { diff --git a/webUI/src/i18n/fa_lang.ts b/webUI/src/i18n/fa_lang.ts index 69816622..ae654ba2 100644 --- a/webUI/src/i18n/fa_lang.ts +++ b/webUI/src/i18n/fa_lang.ts @@ -920,5 +920,15 @@ const fa_lang = { numberinput: { invalid_number: "فقط عدد انگلیسی مجاز است" }, + chart: { + download: "دانلود نمودار", + refresh: "نوسازی نمودار", + fullscreen: "تمام‌صفحه", + details: "جزئیات نمودار", + type: "نوع نمودار", + id: "شناسه نمودار", + dataPoints: "تعداد نقاط داده", + created: "تاریخ ایجاد" + }, }; export default fa_lang diff --git a/webUI/src/views/wizard/home.vue b/webUI/src/views/wizard/home.vue index 80e66b73..e399bab2 100644 --- a/webUI/src/views/wizard/home.vue +++ b/webUI/src/views/wizard/home.vue @@ -15,7 +15,28 @@
+ + + {{ formatTime(message.timestamp) }}
@@ -96,16 +117,18 @@