Passed
Pull Request — master (#6534)
by
unknown
19:39 queued 09:06
created

AiController::generateLearnPath()   A

Complexity

Conditions 4
Paths 17

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 22
c 0
b 0
f 0
nc 17
nop 1
dl 0
loc 32
rs 9.568
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\AiProvider\AiProviderFactory;
10
use Chamilo\CoreBundle\Entity\TrackEDefault;
11
use Chamilo\CoreBundle\Entity\TrackEExercise;
12
use Chamilo\CoreBundle\Repository\TrackEAttemptRepository;
13
use Chamilo\CourseBundle\Entity\CQuizAnswer;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Exception;
16
use Question;
17
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
18
use Symfony\Component\HttpFoundation\JsonResponse;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\Routing\Annotation\Route;
21
22
use const FILTER_VALIDATE_BOOLEAN;
23
24
/**
25
 * Controller to handle AI-related functionalities.
26
 */
27
#[Route('/ai')]
28
class AiController  extends AbstractController
29
{
30
    public function __construct(
31
        private readonly AiProviderFactory        $aiProviderFactory,
32
        private readonly TrackEAttemptRepository  $attemptRepo,
33
        private readonly EntityManagerInterface   $em
34
    ) {}
35
36
    #[Route('/generate_aiken', name: 'chamilo_core_ai_generate_aiken', methods: ['POST'])]
37
    public function generateAiken(Request $request): JsonResponse
38
    {
39
        try {
40
            $data = json_decode($request->getContent(), true);
41
            $nQ = (int) ($data['nro_questions'] ?? 0);
42
            $language = (string) ($data['language'] ?? 'en');
43
            $topic = trim((string) ($data['quiz_name'] ?? ''));
44
            $questionType = $data['question_type'] ?? 'multiple_choice';
45
            $aiProvider = $data['ai_provider'] ?? null;
46
47
            if ($nQ <= 0 || empty($topic)) {
48
                return new JsonResponse([
49
                    'success' => false,
50
                    'text' => 'Invalid request parameters. Ensure all fields are filled correctly.',
51
                ], 400);
52
            }
53
54
            $aiService = $this->aiProviderFactory->getProvider($aiProvider);
55
            $questions = $aiService->generateQuestions($topic, $nQ, $questionType, $language);
56
57
            if (str_starts_with($questions, 'Error:')) {
58
                return new JsonResponse([
59
                    'success' => false,
60
                    'text' => $questions,
61
                ], 500);
62
            }
63
64
            return new JsonResponse([
65
                'success' => true,
66
                'text' => trim($questions),
67
            ]);
68
        } catch (Exception $e) {
69
            error_log('AI Request failed: '.$e->getMessage());
70
71
            return new JsonResponse([
72
                'success' => false,
73
                'text' => 'An error occurred while generating questions. Please contact the administrator.',
74
            ], 500);
75
        }
76
    }
77
78
    #[Route("/open_answer_grade", name:"chamilo_core_ai_open_answer_grade", methods:["POST"])]
79
    public function openAnswerGrade(Request $request): JsonResponse
80
    {
81
        $exeId      = $request->request->getInt('exeId', 0);
82
        $questionId = $request->request->getInt('questionId', 0);
83
        $courseId   = $request->request->getInt('courseId', 0);
84
85
        if (0 === $exeId || 0 === $questionId || 0 === $courseId) {
86
            return $this->json(['error' => 'Missing parameters'], 400);
87
        }
88
89
        /** @var TrackEExercise $trackExercise */
90
        $trackExercise = $this->em
91
            ->getRepository(TrackEExercise::class)
92
            ->find($exeId);
93
        if (null === $trackExercise) {
94
            return $this->json(['error' => 'Exercise attempt not found'], 404);
95
        }
96
97
        $attempt = $this->attemptRepo->findOneBy([
98
            'trackExercise' => $trackExercise,
99
            'questionId'    => $questionId,
100
            'user'          => $trackExercise->getUser()
101
        ]);
102
        if (null === $attempt) {
103
            return $this->json(['error' => 'Attempt not found'], 404);
104
        }
105
106
        $answerText = $attempt->getAnswer();
107
108
        if (ctype_digit($answerText)) {
109
            $cqa = $this->em
110
                ->getRepository(CQuizAnswer::class)
111
                ->find((int) $answerText);
112
            if ($cqa) {
113
                $answerText = $cqa->getAnswer();
114
            }
115
        }
116
117
        $courseInfo = api_get_course_info_by_id($courseId);
118
        if (empty($courseInfo['real_id'])) {
119
            return $this->json(['error' => 'Course info not found'], 500);
120
        }
121
122
        $question = Question::read($questionId, $courseInfo);
123
        if (false === $question) {
124
            return $this->json(['error' => 'Question not found'], 404);
125
        }
126
127
        $language     = $courseInfo['language'] ?? 'en';
128
        $courseTitle  = $courseInfo['title']    ?? '';
129
        $maxScore     = $question->selectWeighting();
130
        $questionText = $question->selectTitle();
131
        $prompt = sprintf(
132
            "In language %s, for the question: '%s', in the context of %s, provide a score between 0 and %d on one line, then feedback on the next line for the following answer: '%s'.",
133
            $language, $questionText, $courseTitle, $maxScore, $answerText
134
        );
135
136
        $raw = trim((string) $this->aiProviderFactory
137
            ->getProvider()
138
            ->gradeOpenAnswer($prompt, 'open_answer_grade'));
139
140
        if ('' === $raw) {
141
            return $this->json(['error' => 'AI request failed'], 500);
142
        }
143
144
        if (false !== strpos($raw, "\n")) {
145
            [$scoreLine, $feedback] = explode("\n", $raw, 2);
146
        } else {
147
            $scoreLine = (string) $maxScore;
148
            $feedback  = $raw;
149
        }
150
151
        $score = (int) filter_var($scoreLine, FILTER_SANITIZE_NUMBER_INT);
152
153
        $track = new TrackEDefault();
154
        $track
155
            ->setDefaultUserId($this->getUser()->getId())
156
            ->setDefaultEventType('ai_use_question_feedback')
157
            ->setDefaultValueType('attempt_id')
158
            ->setDefaultValue((string)$attempt->getId())
159
            ->setDefaultDate(new \DateTime())
160
            ->setCId($courseId)
161
            ->setSessionId(api_get_session_id());
162
        $this->em->persist($track);
163
        $this->em->flush();
164
165
        return $this->json([
166
            'score'    => $score,
167
            'feedback' => $feedback
168
        ]);
169
    }
170
}
171