feat: elearning_agent — reduce tools 14 → 8 so it registers at startup
- Merge get_course_stats + get_enrolled_users + get_slide_completion → get_course_details - Fold publish_course into update_course via website_published param - Drop flag_low_completion (replaced by post_chatter_note) and suggest_next_course (still callable internally via peer-bus suggest_courses request) - elearning_tools: add get_course_details(), extend update_course() signature - ARCHITECTURE.md: mark elearning_agent as registered Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -216,8 +216,12 @@ Each agent follows the same lifecycle: `execute()` → `_plan()` → `_act()`
|
||||
| `employees_agent` | `hr.employee`, `hr.leave` | Headcount, leave, HR queries |
|
||||
| `odoo_doc_agent` | RAG (Qdrant) | Odoo documentation Q&A |
|
||||
|
||||
**`elearning_agent`** — defined but not registered: exceeds the 8-tool limit per
|
||||
agent. Needs to be split into contextual tool groups before it can be used.
|
||||
**`elearning_agent`** — registered. Reduced from 14 → 8 tools by merging the three
|
||||
per-course read calls (`get_course_stats`, `get_enrolled_users`, `get_slide_completion`)
|
||||
into `get_course_details`, folding `publish_course` into `update_course` via a
|
||||
`website_published` param, and dropping `flag_low_completion` (superseded by
|
||||
`post_chatter_note`) and `suggest_next_course` (still available internally via the
|
||||
peer-bus `suggest_courses` request).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,15 +10,9 @@ ELEARNING_TOOLS = [
|
||||
{'name': 'get_courses', 'description': 'List eLearning courses',
|
||||
'parameters': {'active': {'type': 'boolean', 'optional': True},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_course_stats', 'description': 'Get detailed stats for a course',
|
||||
{'name': 'get_course_details',
|
||||
'description': 'Get full details for a course: stats, enrolled users, and per-slide completion',
|
||||
'parameters': {'channel_id': {'type': 'integer'}}},
|
||||
{'name': 'get_enrolled_users', 'description': 'Get users enrolled in a course',
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
'limit': {'type': 'integer', 'optional': True}}},
|
||||
{'name': 'get_slide_completion', 'description': 'Get slide completion rates by user',
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
'min_completion': {'type': 'number', 'optional': True}}},
|
||||
{'name': 'get_learning_summary', 'description': 'Get overall eLearning summary', 'parameters': {}},
|
||||
# ── Create / Write ─────────────────────────────────────────────────────
|
||||
{'name': 'create_course',
|
||||
'description': 'Create a new eLearning course (slide.channel). Returns the new course id.',
|
||||
@@ -27,14 +21,14 @@ ELEARNING_TOOLS = [
|
||||
'enroll_policy': {'type': 'string', 'optional': True},
|
||||
'website_published': {'type': 'boolean', 'optional': True}}},
|
||||
{'name': 'update_course',
|
||||
'description': 'Update name, description, or enroll_policy on an existing course',
|
||||
'description': (
|
||||
'Update an existing course. Pass website_published=True to publish it to the website.'
|
||||
),
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
'name': {'type': 'string', 'optional': True},
|
||||
'description': {'type': 'string', 'optional': True},
|
||||
'enroll_policy': {'type': 'string', 'optional': True}}},
|
||||
{'name': 'publish_course',
|
||||
'description': 'Publish a course to the website (sets website_published=True)',
|
||||
'parameters': {'channel_id': {'type': 'integer'}}},
|
||||
'enroll_policy': {'type': 'string', 'optional': True},
|
||||
'website_published': {'type': 'boolean', 'optional': True}}},
|
||||
{'name': 'add_section',
|
||||
'description': 'Add a section (category) to a course. Sections group slides into topics.',
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
@@ -58,14 +52,8 @@ ELEARNING_TOOLS = [
|
||||
'parameters': {'channel_id': {'type': 'integer'},
|
||||
'partner_id': {'type': 'integer'}}},
|
||||
# ── Notify ─────────────────────────────────────────────────────────────
|
||||
{'name': 'flag_low_completion',
|
||||
'description': 'Post a chatter flag on a course with low completion rate',
|
||||
'parameters': {'channel_id': {'type': 'integer'}, 'reason': {'type': 'string'}}},
|
||||
{'name': 'suggest_next_course',
|
||||
'description': 'Suggest the next course for a learner based on completed courses',
|
||||
'parameters': {'partner_id': {'type': 'integer'}}},
|
||||
{'name': 'post_chatter_note',
|
||||
'description': 'Post a note on any Odoo record',
|
||||
'description': 'Post a note or flag on any Odoo record (use model="slide.channel" for courses)',
|
||||
'parameters': {'model': {'type': 'string'}, 'record_id': {'type': 'integer'},
|
||||
'note': {'type': 'string'}}},
|
||||
]
|
||||
@@ -84,7 +72,7 @@ When asked to build a course, follow this exact workflow:
|
||||
- 'webpage' — rich HTML lesson (provide html_content with the lesson text)
|
||||
- 'video' — video lesson (provide url)
|
||||
- 'quiz' — assessment / knowledge check
|
||||
4. Call publish_course if the task requests a live/published course.
|
||||
4. Call update_course with website_published=True if the task requests a live/published course.
|
||||
5. Call enroll_user for any specific learners mentioned in the task.
|
||||
|
||||
Always use the actual IDs returned by tool calls — never invent IDs.
|
||||
@@ -124,7 +112,7 @@ class ElearningAgent(BaseAgent):
|
||||
self._gathered['intent'] = plan['intent']
|
||||
channel_id = plan.get('channel_id')
|
||||
if plan['intent'] == 'read' and channel_id:
|
||||
self._gathered['course_stats'] = await self._el.get_course_stats(channel_id=channel_id)
|
||||
self._gathered['course_details'] = await self._el.get_course_details(channel_id=channel_id)
|
||||
else:
|
||||
self._gathered['summary'] = await self._el.get_learning_summary()
|
||||
|
||||
@@ -146,10 +134,10 @@ class ElearningAgent(BaseAgent):
|
||||
return
|
||||
for course in reasoning.get('low_completion', [])[:3]:
|
||||
try:
|
||||
await self._el.flag_low_completion(
|
||||
channel_id=course['id'],
|
||||
reason=f'Completion rate {course.get("completion_rate", 0):.1f}% is below 30% threshold',
|
||||
)
|
||||
note = (f'[AI FLAG] Completion rate {course.get("completion_rate", 0):.1f}%'
|
||||
f' is below 30% threshold')
|
||||
await self._el.post_chatter_note(
|
||||
model='slide.channel', record_id=course['id'], note=note)
|
||||
self._actions_taken.append({'action': 'flag_low_completion', 'course_id': course['id']})
|
||||
except Exception as exc:
|
||||
logger.warning('flag_low_completion failed course_id=%s: %s', course.get('id'), exc)
|
||||
@@ -157,16 +145,16 @@ class ElearningAgent(BaseAgent):
|
||||
async def _report(self) -> AgentReport:
|
||||
parts = []
|
||||
summary = self._gathered.get('summary', {})
|
||||
course_stats = self._gathered.get('course_stats', {})
|
||||
course_details = self._gathered.get('course_details', {})
|
||||
if summary:
|
||||
parts.append(
|
||||
f'eLearning: {summary.get("total_courses", 0)} courses, '
|
||||
f'{summary.get("total_enrollments", 0)} enrollments, '
|
||||
f'{summary.get("avg_completion", 0):.1f}% avg completion.'
|
||||
)
|
||||
if course_stats:
|
||||
ch = course_stats.get('channel', {})
|
||||
parts.append(f'Course "{ch.get("name", "?")}": {course_stats.get("slide_count", 0)} slides.')
|
||||
if course_details:
|
||||
ch = course_details.get('channel', {})
|
||||
parts.append(f'Course "{ch.get("name", "?")}": {course_details.get("slide_count", 0)} slides.')
|
||||
creates = [a for a in self._actions_taken if 'create' in a.get('action', '')]
|
||||
if creates:
|
||||
names = ', '.join(a.get('name', f'id={a.get("id")}') for a in creates)
|
||||
@@ -187,17 +175,8 @@ class ElearningAgent(BaseAgent):
|
||||
async def _tool_get_courses(self, active: bool = True, limit: int = 50) -> list:
|
||||
return await self._el.get_courses(active=active, limit=limit)
|
||||
|
||||
async def _tool_get_course_stats(self, channel_id: int) -> dict:
|
||||
return await self._el.get_course_stats(channel_id=channel_id)
|
||||
|
||||
async def _tool_get_enrolled_users(self, channel_id: int, limit: int = 100) -> list:
|
||||
return await self._el.get_enrolled_users(channel_id=channel_id, limit=limit)
|
||||
|
||||
async def _tool_get_slide_completion(self, channel_id: int, min_completion: float = 0.0) -> list:
|
||||
return await self._el.get_slide_completion(channel_id=channel_id, min_completion=min_completion)
|
||||
|
||||
async def _tool_get_learning_summary(self) -> dict:
|
||||
return await self._el.get_learning_summary()
|
||||
async def _tool_get_course_details(self, channel_id: int) -> dict:
|
||||
return await self._el.get_course_details(channel_id=channel_id)
|
||||
|
||||
async def _tool_create_course(self, name: str, description: str = '',
|
||||
enroll_policy: str = 'public',
|
||||
@@ -210,19 +189,15 @@ class ElearningAgent(BaseAgent):
|
||||
return result
|
||||
|
||||
async def _tool_update_course(self, channel_id: int, name: str = None,
|
||||
description: str = None, enroll_policy: str = None) -> dict:
|
||||
description: str = None, enroll_policy: str = None,
|
||||
website_published: bool = None) -> dict:
|
||||
result = await self._el.update_course(channel_id=channel_id, name=name,
|
||||
description=description, enroll_policy=enroll_policy)
|
||||
description=description, enroll_policy=enroll_policy,
|
||||
website_published=website_published)
|
||||
if result.get('success'):
|
||||
self._actions_taken.append({'action': 'update_course', 'id': channel_id})
|
||||
return result
|
||||
|
||||
async def _tool_publish_course(self, channel_id: int) -> dict:
|
||||
result = await self._el.publish_course(channel_id=channel_id)
|
||||
if result.get('success'):
|
||||
self._actions_taken.append({'action': 'publish_course', 'id': channel_id})
|
||||
return result
|
||||
|
||||
async def _tool_add_section(self, channel_id: int, name: str, sequence: int = 0) -> dict:
|
||||
result = await self._el.add_section(channel_id=channel_id, name=name, sequence=sequence)
|
||||
if result.get('success'):
|
||||
@@ -248,13 +223,6 @@ class ElearningAgent(BaseAgent):
|
||||
'channel_id': channel_id, 'partner_id': partner_id})
|
||||
return result
|
||||
|
||||
async def _tool_flag_low_completion(self, channel_id: int, reason: str) -> dict:
|
||||
success = await self._el.flag_low_completion(channel_id=channel_id, reason=reason)
|
||||
return {'success': success}
|
||||
|
||||
async def _tool_suggest_next_course(self, partner_id: int) -> list:
|
||||
return await self._el.suggest_next_course(partner_id=partner_id)
|
||||
|
||||
async def _tool_post_chatter_note(self, model: str, record_id: int, note: str) -> dict:
|
||||
success = await self._el.post_chatter_note(model=model, record_id=record_id, note=note)
|
||||
return {'success': success}
|
||||
|
||||
@@ -83,10 +83,18 @@ class ElearningTools:
|
||||
return {'id': result.record_id, 'name': name, 'success': True}
|
||||
return {'success': False, 'error': result.error}
|
||||
|
||||
async def get_course_details(self, channel_id: int) -> dict:
|
||||
"""Combined course stats + enrolled users + per-slide completion."""
|
||||
stats = await self.get_course_stats(channel_id=channel_id)
|
||||
enrolled = await self.get_enrolled_users(channel_id=channel_id)
|
||||
completion = await self.get_slide_completion(channel_id=channel_id)
|
||||
return {**stats, 'enrolled_users': enrolled, 'slide_completion': completion}
|
||||
|
||||
async def update_course(self, channel_id: int, name: str | None = None,
|
||||
description: str | None = None,
|
||||
enroll_policy: str | None = None) -> dict:
|
||||
"""Update fields on an existing course."""
|
||||
enroll_policy: str | None = None,
|
||||
website_published: bool | None = None) -> dict:
|
||||
"""Update fields on an existing course. Pass website_published=True to publish."""
|
||||
vals: dict = {}
|
||||
if name is not None:
|
||||
vals['name'] = name
|
||||
@@ -94,16 +102,13 @@ class ElearningTools:
|
||||
vals['description_short'] = description
|
||||
if enroll_policy is not None:
|
||||
vals['enroll_policy'] = enroll_policy
|
||||
if website_published is not None:
|
||||
vals['website_published'] = website_published
|
||||
if not vals:
|
||||
return {'success': False, 'error': 'No values to update'}
|
||||
result = await self._o.write('slide.channel', [channel_id], vals)
|
||||
return {'success': result.success, 'error': result.error}
|
||||
|
||||
async def publish_course(self, channel_id: int) -> dict:
|
||||
"""Set website_published=True on a course."""
|
||||
result = await self._o.write('slide.channel', [channel_id], {'website_published': True})
|
||||
return {'success': result.success, 'error': result.error}
|
||||
|
||||
async def add_section(self, channel_id: int, name: str, sequence: int = 0) -> dict:
|
||||
"""Add a section (category slide with is_category=True) to a course."""
|
||||
result = await self._o.create('slide.slide', {
|
||||
|
||||
Reference in New Issue
Block a user