Skip to content

Commit 9554046

Browse files
MDL-88314 core_ai: Strip think tags from AI-generated text
1 parent 68f0f50 commit 9554046

8 files changed

Lines changed: 111 additions & 4 deletions

File tree

public/ai/classes/aiactions/responses/response_explain_text.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
namespace core_ai\aiactions\responses;
1818

19+
use core_ai\helper;
20+
1921
/**
2022
* Explain text action response class.
2123
*
@@ -71,7 +73,10 @@ public function __construct(
7173
public function set_response_data(array $response): void {
7274
$this->id = $response['id'] ?? null;
7375
$this->fingerprint = $response['fingerprint'] ?? null;
74-
$this->generatedcontent = $response['generatedcontent'] ?? null;
76+
$generatedcontent = $response['generatedcontent'] ?? null;
77+
$this->generatedcontent = $generatedcontent !== null
78+
? helper::strip_reasoning_tags($generatedcontent)
79+
: null;
7580
$this->finishreason = $response['finishreason'] ?? null;
7681
$this->prompttokens = $response['prompttokens'] ?? null;
7782
$this->completiontokens = $response['completiontokens'] ?? null;

public/ai/classes/aiactions/responses/response_generate_text.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
namespace core_ai\aiactions\responses;
1818

19+
use core_ai\helper;
20+
1921
/**
2022
* Generate text action response class.
2123
*
@@ -71,7 +73,10 @@ public function __construct(
7173
public function set_response_data(array $response): void {
7274
$this->id = $response['id'] ?? null;
7375
$this->fingerprint = $response['fingerprint'] ?? null;
74-
$this->generatedcontent = $response['generatedcontent'] ?? null;
76+
$generatedcontent = $response['generatedcontent'] ?? null;
77+
$this->generatedcontent = $generatedcontent !== null
78+
? helper::strip_reasoning_tags($generatedcontent)
79+
: null;
7580
$this->finishreason = $response['finishreason'] ?? null;
7681
$this->prompttokens = $response['prompttokens'] ?? null;
7782
$this->completiontokens = $response['completiontokens'] ?? null;

public/ai/classes/aiactions/responses/response_summarise_text.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
namespace core_ai\aiactions\responses;
1818

19+
use core_ai\helper;
20+
1921
/**
2022
* Summarise text action response class.
2123
*
@@ -71,7 +73,10 @@ public function __construct(
7173
public function set_response_data(array $response): void {
7274
$this->id = $response['id'] ?? null;
7375
$this->fingerprint = $response['fingerprint'] ?? null;
74-
$this->generatedcontent = $response['generatedcontent'] ?? null;
76+
$generatedcontent = $response['generatedcontent'] ?? null;
77+
$this->generatedcontent = $generatedcontent !== null
78+
? helper::strip_reasoning_tags($generatedcontent)
79+
: null;
7580
$this->finishreason = $response['finishreason'] ?? null;
7681
$this->prompttokens = $response['prompttokens'] ?? null;
7782
$this->completiontokens = $response['completiontokens'] ?? null;

public/ai/classes/helper.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace core_ai;
18+
19+
/**
20+
* AI helper class.
21+
*
22+
* @package core_ai
23+
* @copyright 2026 Muhammad Arnaldo <muhammad.arnaldo@moodle.com>
24+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25+
*/
26+
class helper {
27+
/**
28+
* Reasoning tag names to strip from AI-generated text.
29+
*
30+
* Add new tag names here when additional AI models are found to include
31+
* reasoning content in their responses.
32+
*
33+
* @var string[]
34+
*/
35+
public const REASONING_TAGS = [
36+
'think',
37+
];
38+
39+
/**
40+
* Strip reasoning tags from AI-generated content.
41+
*
42+
* Some AI models include reasoning or chain-of-thought content wrapped in
43+
* XML-like tags (e.g. <think>...</think>). This method removes those tags
44+
* and their content so only the final response is returned to the user.
45+
*
46+
* @param string $content The AI-generated content.
47+
* @return string The content with reasoning tags removed.
48+
*/
49+
public static function strip_reasoning_tags(string $content): string {
50+
$pattern = implode('|', array_map('preg_quote', self::REASONING_TAGS));
51+
return trim(preg_replace('/<(' . $pattern . ')>.*?<\/\1>\s*/is', '', $content) ?? $content);
52+
}
53+
}

public/ai/placement/editor/classes/external/generate_text.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public static function execute_returns(): external_function_parameters {
128128
VALUE_REQUIRED,
129129
),
130130
'generatedcontent' => new external_value(
131-
PARAM_TEXT,
131+
PARAM_RAW,
132132
'The text generated by AI.',
133133
VALUE_DEFAULT,
134134
),

public/ai/tests/aiactions/responses/response_explain_text_test.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,17 @@ public function test_set_response_data(): void {
7575
$this->assertEquals($body['prompttokens'], $actionresponse->get_response_data()['prompttokens']);
7676
$this->assertEquals($body['completiontokens'], $actionresponse->get_response_data()['completiontokens']);
7777
}
78+
79+
/**
80+
* Test that reasoning tags are stripped from generated content.
81+
*/
82+
public function test_set_response_data_strips_reasoning_tags(): void {
83+
$actionresponse = new response_explain_text(success: true);
84+
$actionresponse->set_response_data([
85+
'generatedcontent' => '<think>Internal reasoning.</think>The actual response.',
86+
'finishreason' => 'stop',
87+
]);
88+
89+
$this->assertEquals('The actual response.', $actionresponse->get_response_data()['generatedcontent']);
90+
}
7891
}

public/ai/tests/aiactions/responses/response_generate_text_test.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,17 @@ public function test_set_response_data(): void {
7575
$this->assertEquals($body['prompttokens'], $actionresponse->get_response_data()['prompttokens']);
7676
$this->assertEquals($body['completiontokens'], $actionresponse->get_response_data()['completiontokens']);
7777
}
78+
79+
/**
80+
* Test that reasoning tags are stripped from generated content.
81+
*/
82+
public function test_set_response_data_strips_reasoning_tags(): void {
83+
$actionresponse = new response_generate_text(success: true);
84+
$actionresponse->set_response_data([
85+
'generatedcontent' => '<think>Internal reasoning.</think>The actual response.',
86+
'finishreason' => 'stop',
87+
]);
88+
89+
$this->assertEquals('The actual response.', $actionresponse->get_response_data()['generatedcontent']);
90+
}
7891
}

public/ai/tests/aiactions/responses/response_summarise_text_test.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,17 @@ public function test_set_response_data(): void {
7575
$this->assertEquals($body['prompttokens'], $actionresponse->get_response_data()['prompttokens']);
7676
$this->assertEquals($body['completiontokens'], $actionresponse->get_response_data()['completiontokens']);
7777
}
78+
79+
/**
80+
* Test that reasoning tags are stripped from generated content.
81+
*/
82+
public function test_set_response_data_strips_reasoning_tags(): void {
83+
$actionresponse = new response_summarise_text(success: true);
84+
$actionresponse->set_response_data([
85+
'generatedcontent' => '<think>Internal reasoning.</think>The actual response.',
86+
'finishreason' => 'stop',
87+
]);
88+
89+
$this->assertEquals('The actual response.', $actionresponse->get_response_data()['generatedcontent']);
90+
}
7891
}

0 commit comments

Comments
 (0)