Skip to content

Commit e0d3dd1

Browse files
caswelltomclaude
andcommitted
Add remote config bundle (v1.0.8): fetch prompt/model overrides from GitHub JSON
- Add classes/remote_config_manager.php — fetches sola-config.json from GitHub, caches 1 hr via MUC, HTTPS-only, 10s timeout, graceful [] fallback on failure - Add remoteconfig cache definition to db/caches.php - Add remoteconfigurl admin setting to settings.php - Integrate remote config into context_builder.php (system_prompt, instruction_blocks) - Integrate remote config into base_provider.php (model_default fallback) - Add sola-config.json starter file (empty prompt/blocks, model_default: gpt-4o-mini) - Update DEFAULT_URL to saylordotorg/sola-moodle-plugin - Bump version to 1.0.8 (2026030501) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4053d3e commit e0d3dd1

File tree

8 files changed

+160
-8
lines changed

8 files changed

+160
-8
lines changed

classes/context_builder.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,13 @@ public static function build_system_prompt(
9696
$coursecontent = self::build_course_content($courseid);
9797
}
9898

99-
// Get template.
99+
// Get template: local admin setting → remote config → lang string default.
100100
$template = get_config('local_ai_course_assistant', 'systemprompt');
101101
if (empty($template)) {
102-
$template = get_string('settings:systemprompt_default', 'local_ai_course_assistant');
102+
$template = remote_config_manager::get_value(
103+
'system_prompt',
104+
get_string('settings:systemprompt_default', 'local_ai_course_assistant')
105+
);
103106
}
104107

105108
// Replace placeholders.
@@ -155,9 +158,14 @@ public static function build_system_prompt(
155158
$prompt .= self::get_multilingual_instructions($lang);
156159

157160
// Brevity instruction — keep responses scannable in the small widget.
158-
$prompt .= "\n\nKEEP RESPONSES BRIEF: Use 2-4 short sentences or bullet points unless the student "
159-
. "explicitly asks for more detail. After answering, you may add a short follow-up offer like "
160-
. "\"Want me to go deeper on any part?\" — but only occasionally, not every time.";
161+
$remote_blocks = remote_config_manager::get_value('instruction_blocks', []);
162+
if (!empty($remote_blocks['brevity'])) {
163+
$prompt .= "\n\n" . $remote_blocks['brevity'];
164+
} else {
165+
$prompt .= "\n\nKEEP RESPONSES BRIEF: Use 2-4 short sentences or bullet points unless the student "
166+
. "explicitly asks for more detail. After answering, you may add a short follow-up offer like "
167+
. "\"Want me to go deeper on any part?\" — but only occasionally, not every time.";
168+
}
161169

162170
// Truncate if needed.
163171
$prompt = self::truncate_prompt($prompt, $courseid);
@@ -459,9 +467,15 @@ private static function get_role_instructions(string $role): string {
459467
/**
460468
* Get AI literacy instructions to weave into tutoring.
461469
*
470+
* Remote config key: instruction_blocks.ai_literacy
471+
*
462472
* @return string
463473
*/
464474
private static function get_ai_literacy_instructions(): string {
475+
$blocks = remote_config_manager::get_value('instruction_blocks', []);
476+
if (!empty($blocks['ai_literacy'])) {
477+
return "\n\n## AI Literacy\n" . $blocks['ai_literacy'];
478+
}
465479
return "\n\n## AI Literacy\n"
466480
. "As part of your tutoring, naturally weave in AI literacy education. Help students understand:\n"
467481
. "- What AI can and cannot do well (good at pattern recognition, summarization, brainstorming; "
@@ -479,9 +493,15 @@ private static function get_ai_literacy_instructions(): string {
479493
/**
480494
* Get instructions for appending SOLA_NEXT suggestion markers.
481495
*
496+
* Remote config key: instruction_blocks.next_steps
497+
*
482498
* @return string
483499
*/
484500
private static function get_next_steps_instructions(): string {
501+
$blocks = remote_config_manager::get_value('instruction_blocks', []);
502+
if (!empty($blocks['next_steps'])) {
503+
return "\n\n## Suggested Follow-up Actions\n" . $blocks['next_steps'];
504+
}
485505
return "\n\n## Suggested Follow-up Actions\n"
486506
. "After EVERY response, append exactly this marker on its own line at the very end:\n"
487507
. "[SOLA_NEXT]suggestion 1||suggestion 2||suggestion 3||suggestion 4[/SOLA_NEXT]\n\n"

classes/provider/base_provider.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@ public function __construct(array $overrides = []) {
5353
$parts = preg_split('/\s+/', $rawkey);
5454
$this->apikey = count($parts) > 1 ? trim(end($parts)) : $rawkey;
5555

56+
$adminmodel = get_config('local_ai_course_assistant', 'model');
5657
$this->model = !empty($overrides['model'])
5758
? $overrides['model']
58-
: (get_config('local_ai_course_assistant', 'model') ?: $this->get_default_model());
59+
: (!empty($adminmodel)
60+
? $adminmodel
61+
: (\local_ai_course_assistant\remote_config_manager::get_value('model_default') ?: $this->get_default_model()));
5962

6063
$this->temperature = isset($overrides['temperature']) && $overrides['temperature'] !== ''
6164
? (float) $overrides['temperature']

classes/remote_config_manager.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 local_ai_course_assistant;
18+
19+
/**
20+
* Fetches and caches remote SOLA configuration from a GitHub-hosted JSON file.
21+
*
22+
* Allows Saylor to push prompt/config updates without a plugin release.
23+
* Local admin settings always take priority over remote values.
24+
* Falls back to empty array (hardcoded defaults) on any failure.
25+
*
26+
* @package local_ai_course_assistant
27+
* @copyright 2025 AI Course Assistant
28+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29+
*/
30+
class remote_config_manager {
31+
32+
/** Default remote config URL (overridable in plugin settings). */
33+
const DEFAULT_URL = 'https://raw.githubusercontent.com/saylordotorg/sola-moodle-plugin/main/sola-config.json';
34+
35+
/** Cache TTL in seconds (1 hour). */
36+
const CACHE_TTL = 3600;
37+
38+
/**
39+
* Return remote config as decoded array. Returns [] on any failure.
40+
*
41+
* @return array
42+
*/
43+
public static function get(): array {
44+
$cache = \cache::make('local_ai_course_assistant', 'remoteconfig');
45+
$cached = $cache->get('config');
46+
if ($cached !== false) {
47+
return $cached;
48+
}
49+
50+
$url = get_config('local_ai_course_assistant', 'remoteconfigurl') ?: self::DEFAULT_URL;
51+
$url = trim($url);
52+
53+
// Security: only allow HTTPS URLs.
54+
if (!$url || strpos($url, 'https://') !== 0) {
55+
$cache->set('config', []);
56+
return [];
57+
}
58+
59+
$ch = curl_init($url);
60+
curl_setopt_array($ch, [
61+
CURLOPT_RETURNTRANSFER => true,
62+
CURLOPT_FOLLOWLOCATION => true,
63+
CURLOPT_MAXREDIRS => 3,
64+
CURLOPT_TIMEOUT => 10,
65+
CURLOPT_USERAGENT => 'SOLA-Moodle-Plugin/1.0',
66+
]);
67+
$body = curl_exec($ch);
68+
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
69+
curl_close($ch);
70+
71+
if ($code !== 200 || !$body) {
72+
$cache->set('config', []);
73+
return [];
74+
}
75+
76+
$data = json_decode($body, true);
77+
if (!is_array($data)) {
78+
$cache->set('config', []);
79+
return [];
80+
}
81+
82+
$cache->set('config', $data);
83+
return $data;
84+
}
85+
86+
/**
87+
* Get a single key from remote config with a fallback value.
88+
*
89+
* @param string $key Top-level key in the remote config JSON.
90+
* @param mixed $fallback Value to return if key is absent or fetch failed.
91+
* @return mixed
92+
*/
93+
public static function get_value(string $key, $fallback = null) {
94+
$config = self::get();
95+
return $config[$key] ?? $fallback;
96+
}
97+
98+
/**
99+
* Invalidate the remote config cache (e.g. after saving settings).
100+
*
101+
* @return void
102+
*/
103+
public static function invalidate(): void {
104+
\cache::make('local_ai_course_assistant', 'remoteconfig')->delete('config');
105+
}
106+
}

db/caches.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@
4242
'changesincourse',
4343
],
4444
],
45+
// Remote config cache (fetched from GitHub-hosted JSON, 1 hour TTL).
46+
'remoteconfig' => [
47+
'mode' => cache_store::MODE_APPLICATION,
48+
'simplekeys' => true,
49+
'ttl' => 3600, // 1 hour.
50+
],
4551
];

lang/en/local_ai_course_assistant.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
101101
## Safety
102102
Do not engage in abusive, hateful, discriminatory, or inappropriate conversations. Set firm but kind boundaries and redirect to productive topics.';
103+
$string['remoteconfigurl'] = 'Remote config URL';
104+
$string['remoteconfigurl_desc'] = 'URL to a JSON file containing remotely-managed SOLA configuration (system prompt, instruction blocks, model default). Must be HTTPS. Leave blank to use the default GitHub URL. Local admin settings always take priority over remote config values.';
103105
$string['settings:temperature'] = 'Temperature';
104106
$string['settings:temperature_desc'] = 'Controls randomness. Lower values are more focused, higher values more creative. Range: 0.0 to 2.0.';
105107
$string['settings:maxhistory'] = 'Max Conversation History';

settings.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@
8484
get_string('settings:systemprompt_default', 'local_ai_course_assistant')
8585
));
8686

87+
// Remote config URL.
88+
$settings->add(new admin_setting_configtext(
89+
'local_ai_course_assistant/remoteconfigurl',
90+
get_string('remoteconfigurl', 'local_ai_course_assistant'),
91+
get_string('remoteconfigurl_desc', 'local_ai_course_assistant'),
92+
\local_ai_course_assistant\remote_config_manager::DEFAULT_URL,
93+
PARAM_URL
94+
));
95+
8796
// Temperature.
8897
$settings->add(new admin_setting_configtext(
8998
'local_ai_course_assistant/temperature',

sola-config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"_comment": "SOLA remote config — edit this file to push config updates without a plugin release. Deploy changes by committing to main branch.",
3+
"model_default": "gpt-4o-mini",
4+
"system_prompt": "",
5+
"instruction_blocks": {}
6+
}

version.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
defined('MOODLE_INTERNAL') || die();
2626

2727
$plugin->component = 'local_ai_course_assistant';
28-
$plugin->version = 2026030406;
28+
$plugin->version = 2026030501;
2929
$plugin->requires = 2024100700; // Moodle 4.5+.
3030
$plugin->maturity = MATURITY_STABLE;
31-
$plugin->release = '1.0.7';
31+
$plugin->release = '1.0.8';

0 commit comments

Comments
 (0)