headless_lms_chatbot/chatbot_tools/course_scope.rs
1//! Shared course resolution for tools that can act on a course other than the one the chatbot
2//! is on.
3
4use crate::{
5 chatbot_tools::tool_authorization::ToolRequirement, prelude::*,
6 user_context::ChatbotTurnContext,
7};
8
9/// Shared schema description for the `course_id` argument of every course-material tool
10/// (`course_structure`, `document_lookup`) that accepts the lenient sentinel form.
11pub const COURSE_ID_ARGUMENT_DESCRIPTION: &str = "The course whose structure to list. Leave empty to use the course this chatbot is on; a global support chatbot must always pass one.";
12
13/// Resolves which course a material tool acts on: the one the call names, or the chatbot's own
14/// when it names none.
15///
16/// Says nothing about whether the caller may reach that course — the tool's `call_requirements`
17/// authorize the resolved id, so a course the caller has no access to is refused there rather
18/// than here.
19pub fn resolve_course_scope(
20 user_context: &ChatbotTurnContext,
21 requested: Option<Uuid>,
22) -> ChatbotResult<Uuid> {
23 requested.or(user_context.course_id).ok_or_else(|| {
24 chatbot_err!(
25 InvalidToolArguments,
26 "No course_id was given and this chatbot is not on a course. Resolve the course with find_course first.".to_string()
27 )
28 })
29}
30
31/// Material access to `course_id`, or nothing to check when the call names no course at all —
32/// such a call fails on its arguments, which is a clearer answer for the model than a denial.
33pub fn material_requirements(course_id: Option<Uuid>) -> Vec<ToolRequirement> {
34 course_id
35 .map(ToolRequirement::CourseMaterial)
36 .into_iter()
37 .collect()
38}