Skip to content

Commit

Permalink
core_ai providers management page
Browse files Browse the repository at this point in the history
  • Loading branch information
mhughes2k committed Mar 21, 2024
1 parent ba46f41 commit 60221ec
Show file tree
Hide file tree
Showing 21 changed files with 537 additions and 50 deletions.
6 changes: 3 additions & 3 deletions lib/classes/ai/aiclient.php → ai/classes/aiclient.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
namespace core\ai;
namespace core_ai;
require_once($CFG->libdir.'/filelib.php');
use core\ai\AiException;
use core_ai\AiException;
/**
* Base client for AI providers that uses simple http request.
*/
Expand All @@ -11,7 +11,7 @@ class AIClient extends \curl {
*/
private $provider;
public function __construct(
\core\ai\AIProvider $provider
AIProvider $provider
) {
$this->provider = $provider;
$settings = [];
Expand Down
5 changes: 2 additions & 3 deletions lib/classes/ai/aiexception.php → ai/classes/aiexception.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php

namespace core\ai;

namespace core_ai;
class AiException extends \moodle_exception {

}
}
94 changes: 83 additions & 11 deletions lib/classes/ai/aiprovider.php → ai/classes/aiprovider.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php
// We're mocking a core Moodle "AI" Subsystem a la Oauth 2
namespace core\ai;
require_once($CFG->dirroot . "/search/engine/solrrag/lib.php");
namespace core_ai;

use \core\persistent;
use core_course_category;
Expand All @@ -16,13 +15,16 @@ protected static function define_properties()
'name' => [
'type' => PARAM_TEXT
],
'enabled' => [
'type' => PARAM_BOOL
],
'apikey' =>[
'type' => PARAM_ALPHANUMEXT
],
'allowembeddings' => [
'type' => PARAM_BOOL
],
'allowquery' => [
'allowchat' => [
'type' => PARAM_BOOL
],
'baseurl' => [
Expand All @@ -43,7 +45,7 @@ protected static function define_properties()
// What context is this provider attached to.
// If null, it's a global provider.
// If -1 its limited to user's own courses.
'context' => [
'contextid' => [
'type' => PARAM_INT
],
// If true, only courses that the user is enrolled in will be searched.
Expand All @@ -53,6 +55,14 @@ protected static function define_properties()
];
}

/**
* Work out the context path from the site to this AI Provider's context
* @return void
*/
public function get_context_path() {
$context = \context::instance_by_id($this->get('contextid'));
var_dump($context);
}
public function use_for_embeddings(): bool {
return $this->get('allowembeddings');
}
Expand Down Expand Up @@ -173,40 +183,102 @@ public function get_settings_for_user($user) {
* @param $limit
* @return array
*/
public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
public static function get_records($filters = [], $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
global $_ENV;

$records = [];
$fake = new static(0, (object) [
'id' => 1,
'name' => "Fake Open AI Provider",
'name' => "Open AI Provider (hardcoded)",
'enabled' => true,
'allowembeddings' => true,
'allowquery' => true,
'allowchat' => true,
'baseurl' => 'https://api.openai.com/v1/',
'embeddings' => 'embeddings',
'embeddingmodel' => 'text-embedding-3-small',
'completions' => 'chat/completions',
'completionmodel' => 'gpt-4-turbo-preview',
'apikey'=> $_ENV['OPENAIKEY'],
'context' => \context_system::instance()->id,
'contextid' => \context_system::instance()->id,
//null, // Global AI Provider
'onlyenrolledcourses' => true
]);
array_push($records, $fake);
$fake = new static(0, (object) [
'id' => 2,
'name' => "Ollama AI Provider",
'name' => "Ollama AI Provider (hard coded)",
'enabled' => true,
'allowembeddings' => true,
'allowquery' => true,
'allowchat' => true,
'baseurl' => 'http://127.0.0.1:11434/api/',
'embeddings' => 'embeddings',
'embeddingmodel' => '',
'completions' => 'chat',
'completionmodel' => 'llama2',
'context' => null, // Global AI Provider
'contextid' => null, // Global AI Provider
'onlyenrolledcourses' => true
// 'apikey'=> $_ENV['OPENAIKEY']
]);
array_push($records, $fake);
$fake = new static(0, (object) [
'id' => 3,
'name' => "Ollama AI Provider (hard coded) Misc Category only",
'enabled' => true,
'allowembeddings' => true,
'allowchat' => true,
'baseurl' => 'http://127.0.0.1:11434/api/',
'embeddings' => 'embeddings',
'embeddingmodel' => '',
'completions' => 'chat',
'completionmodel' => 'llama2',
'contextid' => 111, // Global AI Provider
'onlyenrolledcourses' => true,
// 'apikey'=> $_ENV['OPENAIKEY']
]);
array_push($records, $fake);

$targetcontextid = $filters['contextid'] ?? null;
$targetcontext = null;
if (is_null($targetcontextid)) {
// debugging("No Context Restriction", DEBUG_DEVELOPER);
unset($filters['contextid']); // Because we need special handling.
} else {
// debugging("Context Restriction: {$targetcontextid}", DEBUG_DEVELOPER);
$targetcontext = \context::instance_by_id($targetcontextid);
}
// debugging(print_r($filters,1), DEBUG_DEVELOPER);
$records = array_filter($records, function($record) use ($filters, $targetcontext) {
$result = true;
foreach($filters as $key => $value) {
if ($key == "contextid") {
$providercontextid = $record->get('contextid');
if ($providercontextid == self::CONTEXT_ALL_MY_COURSES) {
// More problematic.
debugging('Provider needs to be in one of user\'s courses', DEBUG_DEVELOPER);
$result = $result & true;
} else if ($providercontextid == null) {
// System provider so always matches.
debugging("System AI provider", DEBUG_DEVELOPER);
$result = $result & true;
} else {
debugging("Context linked AI provider", DEBUG_DEVELOPER);
$providercontext = \context::instance_by_id(
$providercontextid
);
$ischild = $targetcontext->is_child_of($providercontext, true);
debugging("IS child ". (int)$ischild, DEBUG_DEVELOPER);
$result = $result & $ischild;
}
}else {
debugging('Filtering on '.$key. "' = {$value}", DEBUG_DEVELOPER);
if ($record->get($key) != $value) {
return false;
}
}
}
return $result;
});

return $records;
}

Expand Down
55 changes: 55 additions & 0 deletions ai/classes/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
// We're mocking a core Moodle "AI" Subsystem a la Oauth 2

namespace core_ai;
use core_ai\aiprovider;

/**
* AI Help API.
*/
class api {

const ACTION_ADD_PROVIDER = "add";
const ACTION_REMOVE_PROVIDER = "remove";
const ACTION_EDIT_PROVIDER = "edit";
const ACTION_MANAGE_PROVIDERS = "manage";
/**
* Return a list of AIProviders that are available for specified context.
* @param $context
* @return array
*/
public static function get_all_providers($context = null) {
return array_values(aiprovider::get_records());
}
public static function get_provider(int $id): AIProvider {
$fakes = aiprovider::get_records();
return $fakes[0]; // Open AI
// return $fakes[1]; // Ollama
}

/**
* @param $contextid
* @param $allowchat
* @param $allowembeddings
* @return array
*/
public static function get_providers($contextid = null, $allowchat = null, $allowembeddings = null) {
$requirements = ['contextid','allowchat', 'allowembeddings'];
// Filtering AI providers that are available to $contextid, walking up the
// tree when we only have the contextid the AIProvider is set *on* is going to take
// more work.
$filters = [];
foreach($requirements as $req) {
$reqparam = ${$req};
// Null means we don't consider it.
if (!is_null($reqparam)) {
// True means it must be offered
// false means it must *not* be offered by the provider
$filters[$req] = $reqparam;
}
}
//debugging(print_r($filters,1), DEBUG_DEVELOPER);
$providers = aiprovider::get_records($filters);
return array_values($providers);
}
}
108 changes: 108 additions & 0 deletions ai/classes/output/index_page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

class index_page implements renderable, templatable {

private $providers = null;
function __construct($providers) {
$this->providers = $providers;
}

public function providers_table($providers) {
global $CFG;
$table = new html_table();
$table->head = [
'name',
'context',
'completion',
'embeddings',
'status',
'edit'
];

$table->attributes['class'] = 'admintable generaltable';
$data = [];
$contextcache = [];
$index = 0;
foreach ($providers as $provider) {
$first = false;
if ($index == 0) {
$first = true;
}
$last = false;
if ($index == count($providers) - 1) {
$last = true;
}

$name = $provider->get('name');
$contextid = $provider->get('contextid');
$context = "";
if($contextid >0) {
if (
!isset($contextcache[$contextid])
) {
$contextcache[$contextid] = context::instance_by_id($contextid);
}
$contextinstance = $contextcache[$contextid];
$context = $contextinstance->get_context_name();
} else if ($contextid < 0){
$context = "User's own courses";
} else {
$context = "System";
}
$completion = $provider->get('allowchat');
$embeddings = $provider->get('embeddings');
$status = $provider->get('enabled');

// Set up cells.
$namecell = new html_table_cell($name);
$namecell->header = true;

$contextcell = new html_table_cell($context);
$contextcell->header = true;

$completioncell = new html_table_cell(
$completion
?"yes"//$this->pix_icon('yes', get_string('enabled','ai'))
:"no"//$this->pix_icon('no', get_string('disabled','ai'))
);
$completioncell->header = true;

$embeddingscell = new html_table_cell(
$embeddings
?"yes"//$this->pix_icon('yes', get_string('enabled','ai'), 'ai')
:"no"//$this->pix_icon('no', get_string('disabled','ai'), 'ai')
);
$embeddingscell->header = true;

$statuscell = new html_table_cell($status);
$statuscell->header = true;

$links = "";
// Action links.
$editurl = new moodle_url($CFG->wwwroot . '/ai/index.php',
[
'action' => api::ACTION_EDIT_PROVIDER,
'pid' => $provider->get('id')
]);
$links .= html_writer::link($editurl, 'Edit');
$editcell = new html_table_cell($links);
$row = new html_table_row([
$namecell,
$contextcell,
$completioncell,
$embeddingscell,
$statuscell,
$editcell
]);
$data[] = $row;
}
$table->data = $data;
return html_writer::table($table);
}
public function export_for_template(renderer_base $output) : stdClass{
$data = (object)[
'providers' => array_values($this->providers_table($this->providers))
];
return $data;
}
}
18 changes: 18 additions & 0 deletions ai/classes/renderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
//namespace core_ai\output;

use core_ai\api;

//use \html_table;
//use \html_table_cell;
//use \html_table_row;
//use html_writer;
//use moodle_url;
class core_ai_renderer extends \plugin_renderer_base {

public function render_index_page($indexpage) {
$data = $indexpage->export_for_template($this);
return parent::render_from_template('core_ai/index_page', $data);
}

}
Loading

0 comments on commit 60221ec

Please sign in to comment.