xblock-ai-eval 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_eval/__init__.py +6 -0
- ai_eval/base.py +193 -0
- ai_eval/coding_ai_eval.py +259 -0
- ai_eval/compat.py +75 -0
- ai_eval/llm.py +66 -0
- ai_eval/shortanswer.py +182 -0
- ai_eval/static/README.txt +19 -0
- ai_eval/static/css/coding_ai_eval.css +126 -0
- ai_eval/static/css/shortanswer.css +147 -0
- ai_eval/static/html/marked-iframe.html +7 -0
- ai_eval/static/js/src/coding_ai_eval.js +235 -0
- ai_eval/static/js/src/shortanswer.js +129 -0
- ai_eval/static/js/src/utils.js +41 -0
- ai_eval/templates/coding_ai_eval.html +31 -0
- ai_eval/templates/monaco.html +22 -0
- ai_eval/templates/shortanswer.html +35 -0
- ai_eval/utils.py +84 -0
- xblock_ai_eval-0.2.0.dist-info/LICENSE +202 -0
- xblock_ai_eval-0.2.0.dist-info/METADATA +13 -0
- xblock_ai_eval-0.2.0.dist-info/RECORD +23 -0
- xblock_ai_eval-0.2.0.dist-info/WHEEL +5 -0
- xblock_ai_eval-0.2.0.dist-info/entry_points.txt +3 -0
- xblock_ai_eval-0.2.0.dist-info/top_level.txt +1 -0
ai_eval/__init__.py
ADDED
ai_eval/base.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Base Xblock with AI evaluation."""
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
import pkg_resources
|
|
5
|
+
|
|
6
|
+
from django.utils.translation import gettext_noop as _
|
|
7
|
+
from xblock.core import XBlock
|
|
8
|
+
from xblock.fields import String, Scope, Dict
|
|
9
|
+
from xblock.utils.resources import ResourceLoader
|
|
10
|
+
from xblock.utils.studio_editable import StudioEditableXBlockMixin
|
|
11
|
+
from xblock.validation import ValidationMessage
|
|
12
|
+
|
|
13
|
+
from .compat import get_site_configuration_value
|
|
14
|
+
from .llm import SupportedModels
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@XBlock.wants("settings")
|
|
18
|
+
class AIEvalXBlock(StudioEditableXBlockMixin, XBlock):
|
|
19
|
+
"""
|
|
20
|
+
Base class for Xblocks with AI evaluation
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
USER_KEY = "USER"
|
|
24
|
+
LLM_KEY = "LLM"
|
|
25
|
+
|
|
26
|
+
loader = ResourceLoader(__name__)
|
|
27
|
+
|
|
28
|
+
icon_class = "problem"
|
|
29
|
+
model_api_key = String(
|
|
30
|
+
display_name=_("Chosen model API Key"),
|
|
31
|
+
help=_("Enter the API Key of your chosen model. Not required if your administrator has set it globally."),
|
|
32
|
+
default="",
|
|
33
|
+
scope=Scope.settings,
|
|
34
|
+
)
|
|
35
|
+
model_api_url = String(
|
|
36
|
+
display_name=_("Set your API URL"),
|
|
37
|
+
help=_(
|
|
38
|
+
"Fill this only for LLama. This required with models that don't have an official provider."
|
|
39
|
+
" Example URL: https://model-provider-example/llama3_70b"
|
|
40
|
+
),
|
|
41
|
+
default=None,
|
|
42
|
+
scope=Scope.settings,
|
|
43
|
+
)
|
|
44
|
+
model = String(
|
|
45
|
+
display_name=_("AI model"),
|
|
46
|
+
help=_("Select the AI language model to use."),
|
|
47
|
+
values=[
|
|
48
|
+
{"display_name": model, "value": model} for model in SupportedModels.list()
|
|
49
|
+
],
|
|
50
|
+
Scope=Scope.settings,
|
|
51
|
+
default=SupportedModels.GPT4O.value,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
evaluation_prompt = String(
|
|
55
|
+
display_name=_("Evaluation prompt"),
|
|
56
|
+
help=_(
|
|
57
|
+
"Enter the evaluation prompt given to the model."
|
|
58
|
+
" The question will be inserted right after it."
|
|
59
|
+
" The student's answer would then follow the question. Markdown format can be used."
|
|
60
|
+
),
|
|
61
|
+
default="You are a teacher. Evaluate the student's answer for the following question:",
|
|
62
|
+
multiline_editor=True,
|
|
63
|
+
scope=Scope.settings,
|
|
64
|
+
)
|
|
65
|
+
question = String(
|
|
66
|
+
display_name=_("Question"),
|
|
67
|
+
help=_(
|
|
68
|
+
"Enter the question you would like the students to answer."
|
|
69
|
+
" Markdown format can be used."
|
|
70
|
+
),
|
|
71
|
+
default="",
|
|
72
|
+
multiline_editor=True,
|
|
73
|
+
scope=Scope.settings,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
messages = Dict(
|
|
77
|
+
help=_("Dictionary with chat messages"),
|
|
78
|
+
scope=Scope.user_state,
|
|
79
|
+
default={USER_KEY: [], LLM_KEY: []},
|
|
80
|
+
)
|
|
81
|
+
editable_fields = (
|
|
82
|
+
"display_name",
|
|
83
|
+
"evaluation_prompt",
|
|
84
|
+
"question",
|
|
85
|
+
"model",
|
|
86
|
+
"model_api_key",
|
|
87
|
+
"model_api_url",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
block_settings_key = "ai_eval"
|
|
91
|
+
|
|
92
|
+
def _get_settings(self) -> dict: # pragma: nocover
|
|
93
|
+
"""Get the XBlock settings bucket via the SettingsService."""
|
|
94
|
+
settings_service = self.runtime.service(self, "settings")
|
|
95
|
+
if settings_service:
|
|
96
|
+
return settings_service.get_settings_bucket(self)
|
|
97
|
+
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
def resource_string(self, path):
|
|
101
|
+
"""Handy helper for getting resources from our kit."""
|
|
102
|
+
data = pkg_resources.resource_string(__name__, path)
|
|
103
|
+
return data.decode("utf8")
|
|
104
|
+
|
|
105
|
+
def _get_model_config_value(self, config_parameter: str, obj: Self = None) -> str | None:
|
|
106
|
+
"""
|
|
107
|
+
Get configuration value for the model provider with a fallback chain.
|
|
108
|
+
|
|
109
|
+
Checks for the value in the following order:
|
|
110
|
+
1. XBlock field (model_api_key or model_api_url)
|
|
111
|
+
2. Site configuration
|
|
112
|
+
3. XBlock settings (defined in Django settings)
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
config_parameter: Parameter to retrieve (e.g., "API_KEY" or "API_URL").
|
|
116
|
+
obj: Optional data object for validation context.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The configuration value if found in any of the sources, None otherwise.
|
|
120
|
+
"""
|
|
121
|
+
obj = obj or self
|
|
122
|
+
field_name = f"model_{config_parameter}"
|
|
123
|
+
config_key = f"{SupportedModels(obj.model).name}_{config_parameter.upper()}"
|
|
124
|
+
|
|
125
|
+
# XBlock field
|
|
126
|
+
if value := getattr(obj, field_name, None):
|
|
127
|
+
return str(value)
|
|
128
|
+
|
|
129
|
+
# Site configuration
|
|
130
|
+
if value := get_site_configuration_value(self.block_settings_key, config_key):
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
# XBlock settings
|
|
134
|
+
return self._get_settings().get(config_key)
|
|
135
|
+
|
|
136
|
+
def get_model_api_key(self, obj: Self = None) -> str | None:
|
|
137
|
+
"""Get the API key for the model provider."""
|
|
138
|
+
|
|
139
|
+
return self._get_model_config_value("api_key", obj)
|
|
140
|
+
|
|
141
|
+
def get_model_api_url(self, obj: Self = None) -> str | None:
|
|
142
|
+
"""
|
|
143
|
+
Get the API URL for the model provider.
|
|
144
|
+
"""
|
|
145
|
+
return self._get_model_config_value("api_url", obj)
|
|
146
|
+
|
|
147
|
+
def validate_field_data(self, validation, data):
|
|
148
|
+
"""
|
|
149
|
+
Validate fields.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
if not data.model or data.model not in SupportedModels.list():
|
|
153
|
+
validation.add(
|
|
154
|
+
ValidationMessage(
|
|
155
|
+
ValidationMessage.ERROR,
|
|
156
|
+
_( # pylint: disable=translation-of-non-string
|
|
157
|
+
f"Model field is mandatory and must be one of {', '.join(SupportedModels.list())}"
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if not self.get_model_api_key(data):
|
|
163
|
+
validation.add(
|
|
164
|
+
ValidationMessage(
|
|
165
|
+
ValidationMessage.ERROR, _("Model API key is mandatory, if not set globally by your administrator.")
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if data.model == SupportedModels.LLAMA.value and not self.get_model_api_url(data):
|
|
170
|
+
validation.add(
|
|
171
|
+
ValidationMessage(
|
|
172
|
+
ValidationMessage.ERROR,
|
|
173
|
+
_(
|
|
174
|
+
"API URL field is mandatory when using ollama/llama2, "
|
|
175
|
+
"if not set globally by your administrator."
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if data.model != SupportedModels.LLAMA.value and data.model_api_url:
|
|
181
|
+
validation.add(
|
|
182
|
+
ValidationMessage(
|
|
183
|
+
ValidationMessage.ERROR,
|
|
184
|
+
_("API URL field can be set only when using ollama/llama2."),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not data.question:
|
|
189
|
+
validation.add(
|
|
190
|
+
ValidationMessage(
|
|
191
|
+
ValidationMessage.ERROR, _("Question field is mandatory")
|
|
192
|
+
)
|
|
193
|
+
)
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Coding Xblock with AI evaluation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import traceback
|
|
5
|
+
import pkg_resources
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from django.utils.translation import gettext_noop as _
|
|
9
|
+
from web_fragments.fragment import Fragment
|
|
10
|
+
from xblock.core import XBlock
|
|
11
|
+
from xblock.exceptions import JsonHandlerError
|
|
12
|
+
from xblock.fields import Dict, Scope, String
|
|
13
|
+
from xblock.validation import ValidationMessage
|
|
14
|
+
|
|
15
|
+
from .llm import get_llm_response
|
|
16
|
+
from .base import AIEvalXBlock
|
|
17
|
+
from .utils import (
|
|
18
|
+
submit_code,
|
|
19
|
+
get_submission_result,
|
|
20
|
+
SUPPORTED_LANGUAGE_MAP,
|
|
21
|
+
LanguageLabels,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
USER_RESPONSE = "USER_RESPONSE"
|
|
27
|
+
AI_EVALUATION = "AI_EVALUATION"
|
|
28
|
+
CODE_EXEC_RESULT = "CODE_EXEC_RESULT"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CodingAIEvalXBlock(AIEvalXBlock):
|
|
32
|
+
"""
|
|
33
|
+
TO-DO: document what your XBlock does.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
has_author_view = True
|
|
37
|
+
|
|
38
|
+
display_name = String(
|
|
39
|
+
display_name=_("Display Name"),
|
|
40
|
+
help=_("Name of the component in the studio"),
|
|
41
|
+
default="Coding with AI Evaluation",
|
|
42
|
+
scope=Scope.settings,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
judge0_api_key = String(
|
|
46
|
+
display_name=_("Judge0 API Key"),
|
|
47
|
+
help=_(
|
|
48
|
+
"Enter your the Judge0 API key used to execute code on Judge0."
|
|
49
|
+
" Get your key at https://rapidapi.com/judge0-official/api/judge0-ce."
|
|
50
|
+
),
|
|
51
|
+
default="",
|
|
52
|
+
scope=Scope.settings,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
language = String(
|
|
56
|
+
display_name=_("Programming Language"),
|
|
57
|
+
help=_("The programming language used for this Xblock."),
|
|
58
|
+
values=[
|
|
59
|
+
{"display_name": language, "value": language}
|
|
60
|
+
for language in SUPPORTED_LANGUAGE_MAP
|
|
61
|
+
],
|
|
62
|
+
default=LanguageLabels.Python,
|
|
63
|
+
Scope=Scope.settings,
|
|
64
|
+
)
|
|
65
|
+
messages = Dict(
|
|
66
|
+
help=_("Dictionary with messages"),
|
|
67
|
+
scope=Scope.user_state,
|
|
68
|
+
default={USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
editable_fields = AIEvalXBlock.editable_fields + ("judge0_api_key", "language")
|
|
72
|
+
|
|
73
|
+
def resource_string(self, path):
|
|
74
|
+
"""Handy helper for getting resources from our kit."""
|
|
75
|
+
data = pkg_resources.resource_string(__name__, path)
|
|
76
|
+
return data.decode("utf8")
|
|
77
|
+
|
|
78
|
+
def student_view(self, context=None):
|
|
79
|
+
"""
|
|
80
|
+
The primary view of the CodingAIEvalXBlock, shown to students
|
|
81
|
+
when viewing courses.
|
|
82
|
+
"""
|
|
83
|
+
html = self.loader.render_django_template(
|
|
84
|
+
"/templates/coding_ai_eval.html",
|
|
85
|
+
{
|
|
86
|
+
"self": self,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
frag = Fragment(html)
|
|
91
|
+
frag.add_css(self.resource_string("static/css/coding_ai_eval.css"))
|
|
92
|
+
frag.add_javascript(self.resource_string("static/js/src/utils.js"))
|
|
93
|
+
|
|
94
|
+
frag.add_javascript(self.resource_string("static/js/src/coding_ai_eval.js"))
|
|
95
|
+
|
|
96
|
+
monaco_html = self.loader.render_django_template(
|
|
97
|
+
"/templates/monaco.html",
|
|
98
|
+
{
|
|
99
|
+
"monaco_language": SUPPORTED_LANGUAGE_MAP[self.language].monaco_id,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
marked_html = self.resource_string("static/html/marked-iframe.html")
|
|
103
|
+
js_data = {
|
|
104
|
+
"monaco_html": monaco_html,
|
|
105
|
+
"question": self.question,
|
|
106
|
+
"code": self.messages[USER_RESPONSE],
|
|
107
|
+
"ai_evaluation": self.messages[AI_EVALUATION],
|
|
108
|
+
"code_exec_result": self.messages[CODE_EXEC_RESULT],
|
|
109
|
+
"marked_html": marked_html,
|
|
110
|
+
"language": self.language,
|
|
111
|
+
}
|
|
112
|
+
frag.initialize_js("CodingAIEvalXBlock", js_data)
|
|
113
|
+
return frag
|
|
114
|
+
|
|
115
|
+
def author_view(self, context=None):
|
|
116
|
+
"""
|
|
117
|
+
Create preview to be show to course authors in Studio.
|
|
118
|
+
"""
|
|
119
|
+
if not self.validate():
|
|
120
|
+
fragment = Fragment()
|
|
121
|
+
fragment.add_content(
|
|
122
|
+
_(
|
|
123
|
+
"To ensure this component works correctly, please fix the validation issues."
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return fragment
|
|
127
|
+
|
|
128
|
+
return self.student_view(context=context)
|
|
129
|
+
|
|
130
|
+
def validate_field_data(self, validation, data):
|
|
131
|
+
"""
|
|
132
|
+
Validate fields
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
super().validate_field_data(validation, data)
|
|
136
|
+
|
|
137
|
+
if data.language != LanguageLabels.HTML_CSS and not data.judge0_api_key:
|
|
138
|
+
validation.add(
|
|
139
|
+
ValidationMessage(
|
|
140
|
+
ValidationMessage.ERROR, _("Judge0 API key is mandatory")
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@XBlock.json_handler
|
|
145
|
+
def get_response(self, data, suffix=""): # pylint: disable=unused-argument
|
|
146
|
+
"""Get LLM feedback."""
|
|
147
|
+
|
|
148
|
+
answer = f"""
|
|
149
|
+
student code :
|
|
150
|
+
|
|
151
|
+
{data['code']}
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
# stdout and stderr only for executable languages (non HTML)
|
|
155
|
+
if self.language != LanguageLabels.HTML_CSS:
|
|
156
|
+
answer += f"""
|
|
157
|
+
stdout:
|
|
158
|
+
|
|
159
|
+
{data['stdout']}
|
|
160
|
+
|
|
161
|
+
stderr:
|
|
162
|
+
|
|
163
|
+
{data['stderr']}
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
messages = [
|
|
167
|
+
{
|
|
168
|
+
"role": "system",
|
|
169
|
+
"content": f"""
|
|
170
|
+
{self.evaluation_prompt}
|
|
171
|
+
|
|
172
|
+
{self.question}.
|
|
173
|
+
|
|
174
|
+
The programmimg language is {self.language}
|
|
175
|
+
|
|
176
|
+
Evaluation must be in Makrdown format.
|
|
177
|
+
""",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"content": f""" Here is the student's answer:
|
|
181
|
+
{answer}
|
|
182
|
+
""",
|
|
183
|
+
"role": "user",
|
|
184
|
+
},
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
response = get_llm_response(
|
|
189
|
+
self.model,
|
|
190
|
+
self.get_model_api_key(),
|
|
191
|
+
messages,
|
|
192
|
+
self.get_model_api_url(),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
traceback.print_exc()
|
|
197
|
+
logger.error(
|
|
198
|
+
f"Failed while making LLM request using model {self.model}. Eaised error type: {type(e)}, Error: {e}"
|
|
199
|
+
)
|
|
200
|
+
raise JsonHandlerError(500, "A probem occured. Please retry.") from e
|
|
201
|
+
|
|
202
|
+
if response:
|
|
203
|
+
self.messages[USER_RESPONSE] = data["code"]
|
|
204
|
+
self.messages[AI_EVALUATION] = response
|
|
205
|
+
self.messages[CODE_EXEC_RESULT] = {
|
|
206
|
+
"stdout": data["stdout"],
|
|
207
|
+
"stderr": data["stderr"],
|
|
208
|
+
}
|
|
209
|
+
return {"response": response}
|
|
210
|
+
|
|
211
|
+
raise JsonHandlerError(500, "No AI Evaluation available. Please retry.")
|
|
212
|
+
|
|
213
|
+
@XBlock.json_handler
|
|
214
|
+
def submit_code_handler(self, data, suffix=""): # pylint: disable=unused-argument
|
|
215
|
+
"""
|
|
216
|
+
Submit code to Judge0.
|
|
217
|
+
"""
|
|
218
|
+
submission_id = submit_code(
|
|
219
|
+
self.judge0_api_key, data["user_code"], self.language
|
|
220
|
+
)
|
|
221
|
+
return {"submission_id": submission_id}
|
|
222
|
+
|
|
223
|
+
@XBlock.json_handler
|
|
224
|
+
def reset_handler(self, data, suffix=""): # pylint: disable=unused-argument
|
|
225
|
+
"""
|
|
226
|
+
Reset the Xblock.
|
|
227
|
+
"""
|
|
228
|
+
self.messages = {USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}}
|
|
229
|
+
return {"message": "reset successful."}
|
|
230
|
+
|
|
231
|
+
@XBlock.json_handler
|
|
232
|
+
def get_submission_result_handler(
|
|
233
|
+
self, data, suffix=""
|
|
234
|
+
): # pylint: disable=unused-argument
|
|
235
|
+
"""
|
|
236
|
+
Get code submission result.
|
|
237
|
+
"""
|
|
238
|
+
submission_id = data["submission_id"]
|
|
239
|
+
return get_submission_result(self.judge0_api_key, submission_id)
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def workbench_scenarios():
|
|
243
|
+
"""A canned scenario for display in the workbench."""
|
|
244
|
+
return [
|
|
245
|
+
(
|
|
246
|
+
"CodingAIEvalXBlock",
|
|
247
|
+
"""<coding_ai_eval/>
|
|
248
|
+
""",
|
|
249
|
+
),
|
|
250
|
+
(
|
|
251
|
+
"Multiple CodingAIEvalXBlock",
|
|
252
|
+
"""<vertical_demo>
|
|
253
|
+
<coding_ai_eval/>
|
|
254
|
+
<coding_ai_eval/>
|
|
255
|
+
<coding_ai_eval/>
|
|
256
|
+
</vertical_demo>
|
|
257
|
+
""",
|
|
258
|
+
),
|
|
259
|
+
]
|
ai_eval/compat.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Compatibility layer for Open edX."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_current_site_configuration_value(key: str, default: Any = None) -> Any: # pragma: no cover
|
|
9
|
+
"""
|
|
10
|
+
Get value from the current site configuration.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
key: The key to retrieve from the site configuration.
|
|
14
|
+
default: The default value to return if the key is not found.
|
|
15
|
+
Returns:
|
|
16
|
+
The value associated with the key, or the default value.
|
|
17
|
+
"""
|
|
18
|
+
# pylint: disable=import-error,import-outside-toplevel
|
|
19
|
+
from openedx.core.djangoapps.site_configuration.helpers import get_value
|
|
20
|
+
|
|
21
|
+
return get_value(key, default)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_site_configuration_value(domain: str, key: str, default: Any = None) -> Any: # pragma: no cover
|
|
25
|
+
"""
|
|
26
|
+
Get value from the site configuration for a given domain.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
domain: The domain to retrieve site configuration for.
|
|
30
|
+
key: The key to retrieve from the site configuration.
|
|
31
|
+
default: The default value to return if the key is not found.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The value associated with the key, or the default value.
|
|
35
|
+
"""
|
|
36
|
+
# pylint: disable=import-error,import-outside-toplevel
|
|
37
|
+
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
config = SiteConfiguration.objects.get(site__domain=domain).site_values
|
|
41
|
+
return config.get(key, default)
|
|
42
|
+
except SiteConfiguration.DoesNotExist:
|
|
43
|
+
return default
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_site_configuration_value(block_settings_key: str, config_key: str) -> str | None:
|
|
47
|
+
"""
|
|
48
|
+
Retrieve configuration value from site configuration based on execution context.
|
|
49
|
+
|
|
50
|
+
In Open edX, site configurations are defined separately for LMS and CMS (Studio)
|
|
51
|
+
environments. API keys are typically stored in the LMS site configuration.
|
|
52
|
+
This function handles the different contexts:
|
|
53
|
+
|
|
54
|
+
In LMS: Get the API key directly from the current site configuration.
|
|
55
|
+
In CMS: Get the API key using LMS site configuration.
|
|
56
|
+
The LMS domain is retrieved from CMS site configuration or Django settings.
|
|
57
|
+
|
|
58
|
+
This special handling is necessary because when an XBlock is being edited in Studio,
|
|
59
|
+
it needs to access API keys that are stored in the corresponding LMS site configuration,
|
|
60
|
+
not in the Studio site configuration.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
block_settings_key: The key under which block settings are stored.
|
|
64
|
+
config_key: Configuration key to retrieve.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The configuration value if found, None otherwise.
|
|
68
|
+
"""
|
|
69
|
+
if getattr(settings, "SERVICE_VARIANT", None) == "lms":
|
|
70
|
+
block_config = _get_current_site_configuration_value(block_settings_key, {})
|
|
71
|
+
return block_config.get(config_key)
|
|
72
|
+
|
|
73
|
+
lms_base = _get_current_site_configuration_value("LMS_BASE", getattr(settings, "LMS_BASE", None))
|
|
74
|
+
block_config = _get_site_configuration_value(lms_base, block_settings_key, {})
|
|
75
|
+
return block_config.get(config_key)
|
ai_eval/llm.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration with LLMs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from litellm import completion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SupportedModels(Enum):
|
|
10
|
+
"""
|
|
11
|
+
LLM Models supported by the CodingAIEvalXBlock and ShortAnswerAIEvalXBlock
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
GPT4O = "gpt-4o"
|
|
15
|
+
GPT4O_MINI = "gpt-4o-mini"
|
|
16
|
+
GEMINI_PRO = "gemini/gemini-pro"
|
|
17
|
+
CLAUDE_SONNET = "claude-3-5-sonnet-20240620"
|
|
18
|
+
LLAMA = "ollama/llama2"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def list():
|
|
22
|
+
return [str(m.value) for m in SupportedModels]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_llm_response(
|
|
26
|
+
model: SupportedModels, api_key: str, messages: list, api_base: str
|
|
27
|
+
) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Get LLm response.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
model (SupportedModels): The model to use for generating the response. This should be an instance of
|
|
33
|
+
the SupportedModels enum, specifying which LLM model to call.
|
|
34
|
+
api_key (str): The API key required for authenticating with the LLM service. This key should be kept
|
|
35
|
+
confidential and used to authorize requests to the service.
|
|
36
|
+
messages (list): A list of message objects to be sent to the LLM. Each message should be a dictionary
|
|
37
|
+
with the following format:
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
"content": str, # The content of the message. This is the text that you want to send to the LLM.
|
|
41
|
+
"role": str # The role of the message sender. This must be one of the following values:
|
|
42
|
+
# "user" - Represents a user message.
|
|
43
|
+
# "system" - Represents a system message, typically used for instructions or context.
|
|
44
|
+
# "assistant" - Represents a response or message from the LLM itself.
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
[
|
|
49
|
+
{"content": "Hello, how are you?", "role": "user"},
|
|
50
|
+
{"content": "I'm here to help you.", "role": "assistant"}
|
|
51
|
+
]
|
|
52
|
+
api_base (str): The base URL of the LLM API endpoint. This is the root URL used to construct the full
|
|
53
|
+
API request URL. This is required only when using Llama which doesn't have an official provider.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: The response text from the LLM. This is typically the generated output based on the provided
|
|
57
|
+
messages.
|
|
58
|
+
"""
|
|
59
|
+
kwargs = {}
|
|
60
|
+
if api_base:
|
|
61
|
+
kwargs["api_base"] = api_base
|
|
62
|
+
return (
|
|
63
|
+
completion(model=model, api_key=api_key, messages=messages, **kwargs)
|
|
64
|
+
.choices[0]
|
|
65
|
+
.message.content
|
|
66
|
+
)
|