diff --git a/Arqui_AAC.png b/Arqui_AAC.png new file mode 100644 index 000000000..6c857b673 Binary files /dev/null and b/Arqui_AAC.png differ diff --git a/CicloVida_AAC.png b/CicloVida_AAC.png new file mode 100644 index 000000000..fd837a9b2 Binary files /dev/null and b/CicloVida_AAC.png differ diff --git a/backend/lamb/aac/agent/loop.py b/backend/lamb/aac/agent/loop.py index e55705096..ce3ad2b78 100644 --- a/backend/lamb/aac/agent/loop.py +++ b/backend/lamb/aac/agent/loop.py @@ -49,6 +49,51 @@ TEST: lamb test scenarios | add --message "text" | run <id> [--bypass] | runs <id> | evaluate <run_id> <good|bad|mixed> WRITE: lamb assistant create <name> [--system-prompt "..." --llm model ...] | update <id> [...] | delete <id> +## Moodle Integration (if configured) + +When the user has a Moodle integration set up, you can run `moodle ...` commands to query and manage their Moodle LMS. Use `lamb docs read moodle-cli` to see all available commands, or load the `query-moodle` skill for guided interaction. + +Key moodle commands: +READ: moodle course list | get <id> | search <query> | contents <id> +READ: moodle user me | list | get <id> +READ: moodle enrol my-courses | list-users <course_id> +READ: moodle grade get <course_id> | report <course_id> +READ: moodle assign list | submissions <id> +READ: moodle forum list <course_id> | discussions <forum_id> +READ: moodle quiz list <course_id> | attempts <quiz_id> +READ: moodle calendar events +READ: moodle completion status <course_id> +READ: moodle cohort list +READ: moodle site info | functions +WRITE: moodle assign grade | forum post | message send | completion update +WRITE: moodle course create | update | delete +WRITE: moodle user create | update | delete +WRITE: moodle cohort create | delete | add-members | remove-members +WRITE: moodle role assign | unassign +WRITE: moodle calendar create +WRITE: moodle file upload +ESCAPE: moodle call <function_name> -P key=value (any Moodle WS function) + +Always ask the user before running WRITE moodle commands. For READ commands, just run them. + +### TIMESTAMPS — Always convert to readable dates + +Moodle returns Unix timestamps (e.g. `duedate: 1744320000`) for dates. **Always** convert these to a human-readable format (e.g. "Apr 10, 2026 14:30") when presenting results. Never show raw timestamps. Never ask the user "do you want me to convert that?" — just do it. + +Fields that are timestamps: `duedate`, `timestart`, `timecreated`, `timemodified`, `lastaccess`, `timeend`, `timeduration`. + +### PRIVACY — CRITICAL: You can ONLY access YOUR data + +The moodle-cli runs with YOUR Moodle token. You MUST enforce these rules: + +**NEVER query another user by ID.** Commands with `--user-id` or `<user_id>` (`moodle user get <id>`, `moodle grade get --user-id <id>`, `moodle grade report --user-id <id>`, `moodle completion status --user-id <id>`, `moodle message send <user_id>`) may ONLY be used with the authenticated user's own ID. + +**Prefer self-scoped commands:** Use `moodle user me` (not `moodle user get <id>`). Use `moodle enrol my-courses` (not `moodle course list`). + +**Allowed exceptions:** `moodle enrol list-users <course_id>` is OK (teacher context). `moodle course list`, `moodle course get <id>`, `moodle course contents <id>` are OK (course data, not personal data). + +**If the user asks about another user's data**, politely refuse: "I can only access Moodle data for your account. I cannot look up other users' private information." + debug and --bypass = inspect mode. It runs the full prompt assembly (system prompt + RAG context + template) WITHOUT calling the LLM. It returns the constructed messages array — this IS the expected output. An empty or minimal response from debug is NORMAL for non-RAG assistants (no KB content to inject). @@ -104,7 +149,7 @@ When the user asks to do something covered by a specific skill (create, improve, explain, test an assistant), use `lamb skill load <skill-id>` to switch. Available skills: about-lamb, create-assistant, improve-assistant, -explain-assistant, test-and-evaluate. Use `lamb skill list` if unsure. +explain-assistant, test-and-evaluate, setup-moodle, query-moodle. Use `lamb skill list` if unsure. End EVERY response with numbered options. EXACTLY this format, no variations: @@ -192,6 +237,11 @@ "session.rename": "Renaming session", "docs.index": "Loading documentation index", "docs.read": "Reading documentation", + "moodle": "Running Moodle command", + "integration.list": "Checking integrations", + "integration.test": "Testing integration", + "integration.save": "Saving integration", + "integration.remove": "Removing integration", } @@ -200,11 +250,15 @@ def _parse_action_key(cmd: str) -> str: tokens = cmd.strip().split() if tokens and tokens[0] == "lamb": tokens = tokens[1:] + if not tokens: + return "" + # Passthrough commands (like moodle) use just the group name as key + from lamb.aac.liteshell.commands import PASSTHROUGH_COMMANDS + if tokens[0] in PASSTHROUGH_COMMANDS: + return tokens[0] if len(tokens) >= 2 and not tokens[1].startswith("-"): return f"{tokens[0]}.{tokens[1]}" - elif tokens: - return tokens[0] - return "" + return tokens[0] def _describe_tool_call(tc: Any) -> str: @@ -276,6 +330,12 @@ def _summarize_result(action_key: str, result: Any) -> str: elif action_key == "kb.get": files = d.get("files", []) return f"name={d.get('name','?')}, {len(files)} files" + elif action_key == "moodle": + cmd = d.get("command", "") + exit_code = d.get("exit_code", -1) + stdout = d.get("stdout", "") + lines = [l for l in stdout.split("\n") if l.strip()] + return f"exit={exit_code}, {len(lines)} lines, cmd={cmd[:60]}" except Exception: pass @@ -325,6 +385,10 @@ def _extract_artifacts(cmd: str, result: Any) -> list[dict]: if resource_type == "test" and resource_id: return [{"type": "assistant", "id": resource_id, "action": action}] + # For moodle commands, capture the subcommand as the resource type + if resource_type == "moodle" and subcommand: + return [{"type": f"moodle/{subcommand}", "id": resource_id, "action": action}] + if resource_id: return [{"type": resource_type, "id": resource_id, "action": action}] elif resource_type != "help": diff --git a/backend/lamb/aac/authorization.py b/backend/lamb/aac/authorization.py index 4cb0021d2..3c9f6c978 100644 --- a/backend/lamb/aac/authorization.py +++ b/backend/lamb/aac/authorization.py @@ -121,6 +121,10 @@ def resolve_action_key(self, command_str: str) -> str | None: tokens = tokens[1:] if not tokens: return None + # Passthrough commands (like moodle) use just the group name as key + from lamb.aac.liteshell.commands import PASSTHROUGH_COMMANDS + if tokens[0] in PASSTHROUGH_COMMANDS: + return tokens[0] if len(tokens) >= 2 and not tokens[1].startswith("-"): return f"{tokens[0]}.{tokens[1]}" return tokens[0] diff --git a/backend/lamb/aac/docs/index.md b/backend/lamb/aac/docs/index.md index 3cd3328bb..ade6d8237 100644 --- a/backend/lamb/aac/docs/index.md +++ b/backend/lamb/aac/docs/index.md @@ -23,3 +23,4 @@ Use `lamb docs read <topic> --section "heading"` to read a subsection. | collaboration | collaboration.md | Sharing assistants, KBs, and templates with other educators | sharing, shared-with-me, organization, permissions | | troubleshooting | troubleshooting.md | Common problems and solutions for assistants, RAG, publishing, and access | errors, empty-context, rag-broken, cant-publish, no-share-tab, students-cant-access | | glossary | glossary.md | Definitions of key LAMB terms | terms, definitions, vocabulary | +| moodle-cli | moodle-cli.md | Moodle CLI integration — courses, users, grades, enrolments, assignments, forums, quizzes, calendar, messages, completion, files, cohorts, roles | moodle, courses, users, grades, enrolments, assignments, forums, quizzes, calendar, messages, completion, files, cohorts, roles, site | \ No newline at end of file diff --git a/backend/lamb/aac/docs/moodle-cli.md b/backend/lamb/aac/docs/moodle-cli.md new file mode 100644 index 000000000..37988e762 --- /dev/null +++ b/backend/lamb/aac/docs/moodle-cli.md @@ -0,0 +1,187 @@ +--- +topic: moodle-cli +covers: [moodle, courses, users, grades, enrolments, assignments, forums, quizzes, calendar, messages, completion, files, cohorts, roles, site] +answers: + - "how do I list my Moodle courses" + - "show me users enrolled in a course" + - "what grades do my students have" + - "list assignments in a course" + - "show forum discussions" + - "list quizzes in a course" + - "how do I enrol a user" + - "show calendar events" + - "send a message to a user" + - "check activity completion" + - "list cohorts" + - "assign a role to a user" + - "show site information" + - "upload a file to Moodle" +--- + +# moodle-cli: Moodle CLI Integration + +The AAC agent can run `moodle ...` commands to interact with a Moodle LMS instance. These commands require the user to have set up a Moodle integration first (use the `setup-moodle` skill). + +## Authentication + +Before using any moodle command, the user must configure their Moodle credentials: +1. Run the `setup-moodle` skill: `lamb skill load setup-moodle` +2. Follow the steps to provide Moodle URL + Web Services token +3. Verify with `integration test moodle` + +## Available Commands + +### Site information + +| Command | Description | +|---------|-------------| +| `moodle site info` | Show site name, URL, release, version, current user | +| `moodle site functions [--search <term>] [--component <prefix>]` | List available web service functions | + +### Course management + +| Command | Description | +|---------|-------------| +| `moodle course list` | List all courses (ID, short name, full name, visibility) | +| `moodle course get <course_id>` | Get details of a specific course | +| `moodle course search <query>` | Search courses by name | +| `moodle course contents <course_id>` | Show course sections and modules | +| `moodle course create --fullname <name> --shortname <code> --categoryid <id>` | Create a new course | +| `moodle course update <id> [--fullname <name>] [--shortname <code>] [--visible 0\|1]` | Update a course | +| `moodle course delete <id> [id2 ...]` | Delete courses (requires confirmation) | + +### User management + +| Command | Description | +|---------|-------------| +| `moodle user me` | Show current authenticated user info | +| `moodle user list [--key email] [--value %%]` | List/search users (use %% as wildcard) | +| `moodle user get <user_id>` | Get user details by ID | +| `moodle user create --username <name> --firstname <fn> --lastname <ln> --email <email> --password <pw>` | Create a new user | +| `moodle user update <id> [--firstname <fn>] [--lastname <ln>] [--email <email>]` | Update a user | +| `moodle user delete <id> [id2 ...]` | Delete users (requires confirmation) | + +### Enrolment management + +| Command | Description | +|---------|-------------| +| `moodle enrol my-courses` | List courses the current user is enrolled in | +| `moodle enrol list-users <course_id>` | List enrolled users in a course | + +### Grade management + +| Command | Description | +|---------|-------------| +| `moodle grade get <course_id> [--user-id <id>]` | Get grade items for a course | +| `moodle grade report <course_id> [--user-id <id>]` | Get full grade report for a course | + +### Assignment management + +| Command | Description | +|---------|-------------| +| `moodle assign list [--course-id <id>]` | List assignments (optionally filtered by course) | +| `moodle assign submissions <assignment_id> [id2 ...]` | Get submissions for assignment(s) | +| `moodle assign grade --assignment-id <id> --user-id <id> --grade <score> [--feedback <text>]` | Grade a submission | + +### Forum management + +| Command | Description | +|---------|-------------| +| `moodle forum list <course_id>` | List forums in a course | +| `moodle forum discussions <forum_id>` | List discussions in a forum | +| `moodle forum post --forum-id <id> --subject <text> --message <text>` | Post a new discussion to a forum | + +### Quiz management + +| Command | Description | +|---------|-------------| +| `moodle quiz list <course_id>` | List quizzes in a course | +| `moodle quiz attempts <quiz_id> [--user-id <id>]` | List quiz attempts | + +### Calendar management + +| Command | Description | +|---------|-------------| +| `moodle calendar events [--course-id <id>]` | List calendar events (optionally filtered by course) | +| `moodle calendar create --name <name> --timestart <unix_ts> [--duration <secs>] [--description <text>] [--course-id <id>] [--type user\|course\|site]` | Create a calendar event | + +### Messaging + +| Command | Description | +|---------|-------------| +| `moodle message send <user_id> <text>` | Send a message to a user | +| `moodle message list [--from-user <id>]` | List recent messages | +| `moodle message conversations` | List conversations | + +### Activity completion + +| Command | Description | +|---------|-------------| +| `moodle completion status <course_id> [--user-id <id>]` | Show activity completion status for a course | +| `moodle completion update <cmid> <true\|false>` | Manually mark an activity as complete or incomplete | + +### File management + +| Command | Description | +|---------|-------------| +| `moodle file list <contextid> [--component user] [--filearea private] [--itemid 0] [--filepath /]` | List files in a Moodle file area | +| `moodle file upload <local_path> [--component user] [--filearea draft] [--itemid 0]` | Upload a file to Moodle | + +### Cohort management + +| Command | Description | +|---------|-------------| +| `moodle cohort list` | List all cohorts | +| `moodle cohort create --name <name> [--idnumber <code>] [--description <text>]` | Create a cohort | +| `moodle cohort delete <id> [id2 ...]` | Delete cohorts (requires confirmation) | +| `moodle cohort add-members <cohort_id> <user_id> [user_id ...]` | Add users to a cohort | +| `moodle cohort remove-members <cohort_id> <user_id> [user_id ...]` | Remove users from a cohort | + +### Role management + +| Command | Description | +|---------|-------------| +| `moodle role assign --role-id <id> --user-id <id> --context-id <id>` | Assign a role to a user in a context | +| `moodle role unassign --role-id <id> --user-id <id> --context-id <id>` | Unassign a role from a user in a context | + +### Generic API call + +| Command | Description | +|---------|-------------| +| `moodle call <function_name> [-P key=value ...]` | Call any Moodle web service function directly | + +## Usage Tips + +- All commands return structured data. The agent will present it in a readable format. +- **Timestamps**: Moodle returns Unix timestamps (e.g. `duedate: 1744320000`). The agent will always convert these to readable dates automatically — no need to ask. +- For destructive operations (delete, update), the agent will ask for confirmation. +- Use `moodle course list` first to discover course IDs, then drill into specific courses. +- Use `moodle enrol list-users <course_id>` to see who is enrolled in a course. +- Use `moodle grade report <course_id>` to get a full picture of student performance. +- The `moodle call` command is an escape hatch for any Moodle WS function not covered by the other commands. + +## Privacy & Data Access Rules + +**CRITICAL: The moodle-cli runs with the credentials of the LAMB user who configured the Moodle integration. The agent MUST enforce these rules:** + +### Rule 1: Only the authenticated user's data + +All moodle commands execute using **the Moodle token of the user who set up the integration**. The agent may ONLY query data that belongs to or is accessible to this authenticated user. Never query data about a specific different user unless it's in the context of the authenticated user's own courses (e.g. seeing who is enrolled in a course the authenticated user teaches). + +### Rule 2: Never query another user by ID + +Commands that accept a `--user-id` or `<user_id>` parameter (`moodle user get <id>`, `moodle grade get --user-id <id>`, `moodle grade report --user-id <id>`, `moodle completion status --user-id <id>`, `moodle message send <user_id>`) MUST only be used with the authenticated user's own ID. If the user asks about another person's data, explain that you can only show data for the authenticated user. + +Exception: `moodle enrol list-users <course_id>` is allowed because it shows enrolment in a course context (the authenticated user likely has permission to see this as a teacher). + +### Rule 3: Never expose other users' personal data + +If a command returns data about other users (e.g. `moodle user list` shows all users), do NOT display personal details (email, full name) of users other than the authenticated user. Summarize counts instead. + +### Rule 4: Scope queries to the authenticated user + +When the user asks "show me my courses", "what are my grades", etc., use `moodle enrol my-courses` (not `moodle course list`). Use `moodle user me` (not `moodle user get <id>`). Prefer self-scoped commands over general ones. + +### Rule 5: Refuse unauthorized queries + +If the user asks you to look up another specific user's data (grades, profile details, messages, completion status), politely refuse: "I can only access Moodle data for your account. I cannot look up other users' private information." \ No newline at end of file diff --git a/backend/lamb/aac/router.py b/backend/lamb/aac/router.py index b700d0636..067d2d7e8 100644 --- a/backend/lamb/aac/router.py +++ b/backend/lamb/aac/router.py @@ -74,6 +74,8 @@ async def create_session( "explain-assistant": "Explain", "test-and-evaluate": "Test", "test-lti-tools": "LTI Setup", + "setup-moodle": "Setup: Moodle", + "query-moodle": "Moodle Query", } if skill_id: base = _skill_titles.get(skill_id, skill_id) diff --git a/backend/lamb/aac/skills/about_lamb.md b/backend/lamb/aac/skills/about_lamb.md index 8740e91e2..ac8616083 100644 --- a/backend/lamb/aac/skills/about_lamb.md +++ b/backend/lamb/aac/skills/about_lamb.md @@ -20,6 +20,7 @@ Greet briefly (2 lines max). Say you can help with: - Rubrics and evaluation - Publishing to Moodle/LMS - Troubleshooting +- Moodle data queries (courses, users, grades) — **privacy: only your own data** Do NOT list all features. Do NOT dump the docs index. Just offer help and wait. @@ -37,6 +38,8 @@ When the user asks about something they could DO right now, switch to the approp - "Improve my assistant 18" → `lamb skill load improve-assistant --assistant 18` - "Explain how assistant 18 works" → `lamb skill load explain-assistant --assistant 18` - "Test my assistant" → `lamb skill load test-and-evaluate --assistant 18` +- "Set up Moodle" → `lamb skill load setup-moodle` +- "Query Moodle data" → `lamb skill load query-moodle` The skill switch happens seamlessly — the conversation continues, the skill adds expertise. If unsure which skill, use `lamb skill list` to see all available skills. diff --git a/backend/lamb/aac/skills/query_moodle.md b/backend/lamb/aac/skills/query_moodle.md new file mode 100644 index 000000000..e64b5253a --- /dev/null +++ b/backend/lamb/aac/skills/query_moodle.md @@ -0,0 +1,143 @@ +--- +id: query-moodle +name: Query Moodle +description: Consult and manage Moodle data — courses, users, grades, assignments, forums, quizzes, and more +required_context: [] +optional_context: [language] +requires_integration: moodle +startup_actions: + - "lamb docs read moodle-cli" +--- + +# Skill: Query Moodle + +You are a Moodle data assistant. The user has a Moodle integration configured. Help them query and manage their Moodle data using `moodle ...` commands. + +## On startup + +Read the moodle-cli docs (already loaded). Greet briefly (2 lines max). Say you can help with Moodle data — courses, users, grades, assignments, etc. + +If the user hasn't specified what they need, suggest a few starting points: +- "List my courses" → `moodle course list` +- "Who's enrolled in course X?" → `moodle enrol list-users <id>` +- "Show grades for course X" → `moodle grade report <id>` + +## Available command groups + +### READ operations (safe, always available) + +**Courses:** `moodle course list`, `moodle course get <id>`, `moodle course search <query>`, `moodle course contents <id>` +**Users:** `moodle user me`, `moodle user list`, `moodle user get <id>` +**Enrolments:** `moodle enrol my-courses`, `moodle enrol list-users <course_id>` +**Grades:** `moodle grade get <course_id>`, `moodle grade report <course_id>` +**Assignments:** `moodle assign list`, `moodle assign submissions <id>` +**Forums:** `moodle forum list <course_id>`, `moodle forum discussions <forum_id>` +**Quizzes:** `moodle quiz list <course_id>`, `moodle quiz attempts <quiz_id>` +**Calendar:** `moodle calendar events` +**Messages:** `moodle message list`, `moodle message conversations` +**Completion:** `moodle completion status <course_id>` +**Files:** `moodle file list <contextid>` +**Cohorts:** `moodle cohort list` +**Site:** `moodle site info`, `moodle site functions` + +### WRITE operations (ask user before executing) + +**Courses:** `moodle course create`, `moodle course update`, `moodle course delete` +**Users:** `moodle user create`, `moodle user update`, `moodle user delete` +**Assignments:** `moodle assign grade` +**Forums:** `moodle forum post` +**Calendar:** `moodle calendar create` +**Messages:** `moodle message send` +**Completion:** `moodle completion update` +**Files:** `moodle file upload` +**Cohorts:** `moodle cohort create`, `moodle cohort delete`, `moodle cohort add-members`, `moodle cohort remove-members` +**Roles:** `moodle role assign`, `moodle role unassign` + +### Generic escape hatch + +`moodle call <function_name> -P key=value` — call any Moodle web service function directly. Use this when no specific command exists for the needed operation. + +## How to answer questions + +1. **Understand the user's intent** — map their natural language request to a moodle command +2. **Run the command** — use `moodle ...` via the liteshell +3. **Present results clearly** — use tables for lists, summaries for details +4. **Offer next steps** — "Want to see grades for that course? Check enrolments?" + +## Privacy Rules — CRITICAL + +The moodle-cli runs with **YOUR Moodle credentials** (the token you configured). The agent MUST enforce these rules: + +### You can ONLY see YOUR data +- `moodle user me` → ✅ shows YOUR info +- `moodle user get 5` → ❌ shows ANOTHER user's info — REFUSE +- `moodle enrol my-courses` → ✅ shows YOUR courses +- `moodle grade report <course_id> --user-id 5` → ❌ shows another student's grades — REFUSE + +### Commands that are ALWAYS safe +- `moodle course list` — lists all courses (no personal data) +- `moodle course get <id>` — course details (no personal data) +- `moodle course contents <id>` — course materials (no personal data) +- `moodle enrol list-users <course_id>` — shows who's enrolled (teacher context, OK) +- `moodle site info` — site info (no personal data) +- `moodle calendar events` — your calendar events +- `moodle cohort list` — cohort list (no personal data) + +### Commands that are ONLY safe for YOUR user ID +- `moodle user get <id>` — ONLY if `<id>` is YOUR user ID +- `moodle grade get <course_id> --user-id <id>` — ONLY if `<id>` is YOUR user ID +- `moodle grade report <course_id> --user-id <id>` — ONLY if `<id>` is YOUR user ID +- `moodle completion status <course_id> --user-id <id>` — ONLY if `<id>` is YOUR user ID +- `moodle message send <user_id>` — ONLY to YOUR user ID +- `moodle message list --from-user <id>` — ONLY if `<id>` is YOUR user ID + +### How to handle requests about other users + +| User says | Your response | +|-----------|---------------| +| "Show me user 5's profile" | "I can only access your own Moodle profile. Use `moodle user me` to see your info." | +| "What grades does student 7 have?" | "I can only see your own grades. I cannot look up other users' private data." | +| "Show messages from user 3" | "I can only show your own messages." | +| "What's the completion status of user 8?" | "I can only check your own completion status." | +| "Who's in course 3?" | ✅ "Let me check enrolments..." → `moodle enrol list-users 3` (this is OK — teacher context) | + +### General principle +If a command accepts `--user-id` and the user asks about someone other than themselves, **refuse politely**. Say: "I can only access Moodle data for your account. I cannot look up other users' private information." + +## Examples + +| User says | Command to run | +|-----------|---------------| +| "What courses do I have?" | `moodle course list` | +| "Show me course 5" | `moodle course get 5` | +| "Who's in course 3?" | `moodle enrol list-users 3` | +| "Grades for course 3" | `moodle grade report 3` | +| "List assignments" | `moodle assign list` | +| "Show forums in course 3" | `moodle forum list 3` | +| "Quizzes in course 3" | `moodle quiz list 3` | +| "Calendar events" | `moodle calendar events` | +| "Send a message to user 7" | `moodle message send 7 "text"` | +| "Completion status for course 3" | `moodle completion status 3` | +| "List cohorts" | `moodle cohort list` | +| "Site info" | `moodle site info` | +| "Upload a file" | `moodle file upload <path>` | + +## Troubleshooting + +- **"moodle-cli is not installed"** → the binary isn't in PATH. The backend container needs `pip install -e ./cli-plugins/moodle-cli`. +- **"No token for profile 'default'"** → the user's integration credentials aren't being injected. Run `integration list` to check, then `integration test moodle` to verify. +- **"Moodle API error"** → the Moodle instance returned an error. Show the error message to the user and suggest checking the Moodle logs. +- **Timeout** → Moodle might be slow or unreachable. Suggest the user check their Moodle URL. + +## Language + +If a `language` context is set, respond in that language. Otherwise match the user's language. + +## Style + +- Be concise and direct +- Use tables for lists of items +- For single items, use a brief summary +- End with numbered options for next steps +- Do NOT explain the command syntax unless the user asks +- **Timestamps**: Always convert Unix timestamps to readable dates (e.g. "Apr 10, 2026 14:30"). Never show raw numbers. Never ask the user if they want conversion — just do it automatically. \ No newline at end of file diff --git a/backend/lamb/aac/skills/setup_moodle.md b/backend/lamb/aac/skills/setup_moodle.md index e826b01b9..135b6f864 100644 --- a/backend/lamb/aac/skills/setup_moodle.md +++ b/backend/lamb/aac/skills/setup_moodle.md @@ -57,7 +57,12 @@ If the test fails: - Summarise: "Moodle connected: {site} as {username}. You can now ask me things like *list my courses* or *who's enrolled in course X*, and I'll use `moodle ...` on your behalf." - Rename the session with `lamb session rename "Setup: Moodle ({site})"` so it's findable later. -- Offer one concrete next step, e.g. `moodle course list` or suggest loading a skill that uses Moodle. +- Offer to load the `query-moodle` skill for guided Moodle queries: `lamb skill load query-moodle` +- Or offer one concrete next step, e.g. `moodle course list` or `moodle enrol list-users <course_id>` + +## Privacy reminder + +Remind the user that the Moodle integration uses **their** Moodle token. The agent can only access data that **their Moodle account** has permission to see. The agent will never look up another user's private data (grades, profile, messages) — if asked, it will refuse. ## What NOT to do diff --git a/backend/tests/test_aac_moodle.py b/backend/tests/test_aac_moodle.py new file mode 100644 index 000000000..2c33fe765 --- /dev/null +++ b/backend/tests/test_aac_moodle.py @@ -0,0 +1,149 @@ +""" +Tests for AAC Moodle command passthrough and integration support in the pastor branch. + +These tests verify the `moodle` passthrough command in liteshell, credential +injection from the per-user integrations store, and command key resolution for +passthrough commands. +""" + +import os +import sys +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +# Ensure backend is on sys.path when running from repository root +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from lamb.aac.authorization import ActionAuthorizer, classify_user_confirmation +from lamb.aac.liteshell.commands import moodle_cmd +from lamb.aac.liteshell.shell import CommandContext, LiteShell + + +class DummyProcess: + def __init__(self, returncode=0, stdout="ok", stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +def _make_ctx() -> CommandContext: + return CommandContext( + http=None, + server_url="http://localhost", + token="fake-token", + user_email="tester@example.com", + organization_id=1, + user_id=42, + ) + + +@patch("subprocess.run") +@patch("shutil.which", return_value="/usr/bin/moodle") +@patch("lamb.aac.liteshell.commands._integrations_store") +def test_moodle_cmd_uses_user_integration_credentials( + mock_store_factory, mock_which, mock_run +): + """moodle_cmd injects stored Moodle credentials into the subprocess environment.""" + mock_store = MagicMock() + mock_store.get_config.return_value = { + "url": "https://moodle.example.com", + "token": "token123", + "service": "moodle_mobile_app", + } + mock_store_factory.return_value = mock_store + mock_run.return_value = DummyProcess(returncode=0, stdout="success", stderr="") + + ctx = _make_ctx() + result = moodle_cmd(ctx, ["course", "list"], {}) + + assert result["exit_code"] == 0 + assert result["stdout"] == "success" + assert result["credential_source"] == "user_integration" + mock_run.assert_called_once() + args, kwargs = mock_run.call_args + assert args[0] == ["/usr/bin/moodle", "course", "list"] + assert kwargs["env"]["MOODLE_URL"] == "https://moodle.example.com" + assert kwargs["env"]["MOODLE_TOKEN"] == "token123" + assert kwargs["env"]["MOODLE_SERVICE"] == "moodle_mobile_app" + + +@patch("subprocess.run") +@patch("shutil.which", return_value="/usr/bin/moodle") +@patch("lamb.aac.liteshell.commands._integrations_store") +def test_moodle_cmd_falls_back_to_ambient_env( + mock_store_factory, mock_which, mock_run +): + """moodle_cmd uses ambient environment when no user integration is configured.""" + mock_store = MagicMock() + mock_store.get_config.return_value = None + mock_store_factory.return_value = mock_store + mock_run.return_value = DummyProcess(returncode=0, stdout="ambient", stderr="") + + ctx = _make_ctx() + result = moodle_cmd(ctx, ["assignment", "list", "--course", "10"], {}) + + assert result["credential_source"] == "ambient" + assert result["command"] == "moodle assignment list --course 10" + mock_run.assert_called_once() + _, kwargs = mock_run.call_args + assert kwargs["env"] is None + + +@patch("shutil.which", return_value=None) +def test_moodle_cmd_raises_when_binary_missing(mock_which): + """moodle_cmd fails cleanly when the moodle CLI binary is not installed.""" + ctx = _make_ctx() + with pytest.raises(RuntimeError, match="moodle-cli is not installed"): + moodle_cmd(ctx, ["course", "list"], {}) + + +@pytest.mark.asyncio +@patch("lamb.aac.liteshell.shell.LiteShell._get_http", return_value=None) +@patch("subprocess.run") +@patch("shutil.which", return_value="/usr/bin/moodle") +@patch("lamb.aac.liteshell.commands._integrations_store") +async def test_liteshell_passes_through_moodle_commands( + mock_store_factory, mock_which, mock_run, mock_get_http +): + """LiteShell routes `moodle ...` commands through the passthrough handler.""" + mock_store = MagicMock() + mock_store.get_config.return_value = { + "url": "https://moodle.example.com", + "token": "token123", + } + mock_store_factory.return_value = mock_store + mock_run.return_value = DummyProcess(returncode=0, stdout="ok", stderr="") + + shell = LiteShell( + server_url="http://localhost", + token="fake-token", + user_email="tester@example.com", + organization_id=1, + user_id=42, + ) + + for command, expected_args in [ + ("lamb moodle course list", ["course", "list"]), + ("lamb moodle assignment list --course 10", ["assignment", "list", "--course", "10"]), + ("lamb moodle report summary", ["report", "summary"]), + ]: + result = await shell.execute(command) + assert result.success is True + assert result.data["command"] == f"moodle {' '.join(expected_args)}" + assert mock_run.call_args[0][0] == ["/usr/bin/moodle", *expected_args] + + +def test_action_authorizer_resolves_moodle_passthrough_key(): + authorizer = ActionAuthorizer() + action_key = authorizer.resolve_action_key("lamb moodle course list") + assert action_key == "moodle" + + +def test_classify_user_confirmation_approval_and_rejection(): + assert classify_user_confirmation("sí") == "approve" + assert classify_user_confirmation("no gracias") == "reject" + assert classify_user_confirmation("please do it") == "approve" + assert classify_user_confirmation("not now") == "reject"