Updated UI and added prompt update mechanism

This commit is contained in:
Viswamedha Nalabotu 2026-03-18 22:04:07 +00:00
parent e818991ae3
commit a594f93154
6 changed files with 147 additions and 29 deletions

View file

@ -112,49 +112,28 @@ class Role(IdentifierMixin, TimeStampMixin, Model):
def create_default_agents_for_role(sender, instance: Role, created: bool, **kwargs): def create_default_agents_for_role(sender, instance: Role, created: bool, **kwargs):
if created: if created:
from apps.onboarding.models import AgentConfig # L: circular import :( from apps.onboarding.models import AgentConfig # L: circular import :(
from apps.onboarding.consumers.prompts import OnboardingPrompts
default_agents = [ default_agents = [
{ {
'type': 'curriculum', 'type': 'curriculum',
'name': f"{instance.name} Curriculum Agent", 'name': f"{instance.name} Curriculum Agent",
'prompt': ( 'prompt': OnboardingPrompts.default_curriculum_prompt(instance.name),
f"You are an instructional design assistant for onboarding the role '{instance.name}'. "
"Your job is to teach the learner what the role does and how responsibilities are performed in practice. "
"Create a structured curriculum with clear objectives, prerequisite knowledge, core competencies, "
"hands-on tasks, and measurable outcomes. Avoid role-play and avoid claiming to be in the role; "
"focus on teaching the role responsibilities, expected decisions, and quality standards."
)
}, },
{ {
'type': 'knowledge', 'type': 'knowledge',
'name': f"{instance.name} Knowledge Agent", 'name': f"{instance.name} Knowledge Agent",
'prompt': ( 'prompt': OnboardingPrompts.default_knowledge_prompt(instance.name),
f"You are a domain knowledge tutor for the role '{instance.name}'. "
"Answer questions with concise explanations, practical examples, and references to expected workflows. "
"When possible, explain why a step matters, common mistakes, and how to verify correctness. "
"Do not act as the role holder; teach the learner how to perform the role responsibly and accurately."
)
}, },
{ {
'type': 'assessment', 'type': 'assessment',
'name': f"{instance.name} Assessment Agent", 'name': f"{instance.name} Assessment Agent",
'prompt': ( 'prompt': OnboardingPrompts.default_assessment_prompt(instance.name),
f"You are an assessment designer for onboarding the role '{instance.name}'. "
"Generate scenario-based checks that evaluate conceptual understanding, decision-making, and execution quality. "
"Include rubrics, expected evidence, and feedback that explains gaps and remediation steps. "
"Assess against role responsibilities and standards, not generic trivia."
)
}, },
{ {
'type': 'monitor', 'type': 'monitor',
'name': f"{instance.name} Progress Monitor", 'name': f"{instance.name} Progress Monitor",
'prompt': ( 'prompt': OnboardingPrompts.default_monitor_prompt(instance.name),
f"You are a progress coaching assistant for learners training for the role '{instance.name}'. " },
"Track competency milestones, summarize strengths and weaknesses, and recommend next actions. "
"Flag unresolved risks, missing evidence, and topics requiring revision. "
"Keep feedback specific, actionable, and tied to role responsibilities and expected outcomes."
)
}
] ]
with transaction.atomic(): with transaction.atomic():

View file

@ -1,4 +1,5 @@
import hashlib import hashlib
import logging
from celery import shared_task from celery import shared_task
from django.conf import settings from django.conf import settings
@ -9,6 +10,8 @@ from pypdf import PdfReader
from apps.knowledge.models import RoleRagDocument, TrainingFile from apps.knowledge.models import RoleRagDocument, TrainingFile
logger = logging.getLogger(__name__)
def _decode_text_bytes(raw_bytes: bytes) -> str: def _decode_text_bytes(raw_bytes: bytes) -> str:
try: try:
@ -41,6 +44,10 @@ def _get_text_chunks(text: str, size: int = 10000):
@shared_task(name="apps.knowledge.tasks.ingest_training_file_task", bind=True, soft_time_limit=900, time_limit=1200) @shared_task(name="apps.knowledge.tasks.ingest_training_file_task", bind=True, soft_time_limit=900, time_limit=1200)
def ingest_training_file_task(self, file_uuid): def ingest_training_file_task(self, file_uuid):
"""
Ingests a training file by extracting text, chunking it, generating embeddings via an external service,
and saving RoleRagDocument entries. Updates the file status accordingly and triggers prompt refinement.
"""
try: try:
file_obj = TrainingFile.objects.get(uuid=file_uuid) file_obj = TrainingFile.objects.get(uuid=file_uuid)
except TrainingFile.DoesNotExist: except TrainingFile.DoesNotExist:
@ -99,6 +106,10 @@ def ingest_training_file_task(self, file_uuid):
file_obj.status = 'embedded' file_obj.status = 'embedded'
file_obj.is_processed = True file_obj.is_processed = True
file_obj.save() file_obj.save()
if file_obj.role_id:
update_agent_prompts_from_file_task.delay(str(file_obj.role.uuid))
return f"Processed {chunk_counter} chunks via batching." return f"Processed {chunk_counter} chunks via batching."
except Exception as e: except Exception as e:
@ -106,3 +117,69 @@ def ingest_training_file_task(self, file_uuid):
file_obj.description = str(e) file_obj.description = str(e)
file_obj.save() file_obj.save()
raise e raise e
@shared_task(name="apps.knowledge.tasks.update_agent_prompts_from_file_task", bind=True, soft_time_limit=120, time_limit=180)
def update_agent_prompts_from_file_task(self, role_uuid: str):
"""
After a training file is ingested (or deleted), refine the curriculum AgentConfig
system prompt using document content. Resets to the canonical base prompt when no
files remain.
"""
from apps.accounts.models import Role
from apps.onboarding.consumers.prompts import OnboardingPrompts
from apps.onboarding.models import AgentConfig
try:
role = Role.objects.get(uuid=role_uuid)
except Role.DoesNotExist:
logger.warning("update_agent_prompts_from_file_task: role %s not found", role_uuid)
return
curriculum_config = AgentConfig.objects.filter(role=role, agent_type='curriculum').first()
if not curriculum_config:
logger.warning("update_agent_prompts_from_file_task: no curriculum config for role %s", role_uuid)
return
chunk_texts = list(
RoleRagDocument.objects.filter(role=role, is_active=True)
.order_by('training_file_id', 'chunk_index')
.values_list('content', flat=True)[:30]
)
# No files left... so we should reset
if not chunk_texts:
curriculum_config.system_prompt = OnboardingPrompts.default_curriculum_prompt(role.name)
curriculum_config.save(update_fields=['system_prompt', 'updated_at'])
logger.info("update_agent_prompts_from_file_task: reset to base prompt for role %s", role_uuid)
return
combined_text = '\n\n'.join(chunk_texts)[:6000]
base_prompt = OnboardingPrompts.default_curriculum_prompt(role.name)
try:
with Client(timeout=Timeout(60.0)) as client:
response = client.post(
settings.INFERENCE_CHAT_COMPLETIONS_ENDPOINT,
json={
"model": "meta-llama-3.1-8b-instruct",
"messages": [
{
"role": "user",
"content": OnboardingPrompts.refine_curriculum_prompt(
role.name, base_prompt, combined_text
),
},
],
"max_tokens": 600,
},
)
response.raise_for_status()
refined_prompt = response.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
logger.exception("update_agent_prompts_from_file_task: LLM call failed for role %s: %s", role_uuid, e)
return
curriculum_config.system_prompt = refined_prompt
curriculum_config.save(update_fields=['system_prompt', 'updated_at'])
logger.info("update_agent_prompts_from_file_task: refined curriculum prompt for role %s", role_uuid)

View file

@ -8,6 +8,7 @@ from apps.accounts.models import Organization, Role
from apps.accounts.permissions import can_manage_organization from apps.accounts.permissions import can_manage_organization
from apps.knowledge.models import RoleRagDocument, TrainingFile from apps.knowledge.models import RoleRagDocument, TrainingFile
from apps.knowledge.serializers import RoleRagDocumentSerializer, TrainingFileSerializer from apps.knowledge.serializers import RoleRagDocumentSerializer, TrainingFileSerializer
from apps.knowledge.tasks import update_agent_prompts_from_file_task
class TrainingFileViewSet(ModelViewSet): class TrainingFileViewSet(ModelViewSet):
queryset = TrainingFile.objects.all() queryset = TrainingFile.objects.all()
@ -89,7 +90,11 @@ class TrainingFileViewSet(ModelViewSet):
if not (is_uploader or is_org_owner or is_org_manager): if not (is_uploader or is_org_owner or is_org_manager):
raise PermissionDenied('Permission denied') raise PermissionDenied('Permission denied')
return super().destroy(request, *args, **kwargs) role_uuid = str(instance.role.uuid) if instance.role_id else None
response = super().destroy(request, *args, **kwargs)
if role_uuid:
update_agent_prompts_from_file_task.delay(role_uuid)
return response
class RoleRagDocumentViewSet(ReadOnlyModelViewSet): class RoleRagDocumentViewSet(ReadOnlyModelViewSet):
queryset = RoleRagDocument.objects.all() queryset = RoleRagDocument.objects.all()

View file

@ -70,6 +70,57 @@ class OnboardingPrompts:
f"Progress context JSON:\n{json.dumps(progress_context)}" f"Progress context JSON:\n{json.dumps(progress_context)}"
) )
### Default agent system prompts (canonical source of truth) ###
@staticmethod
def default_curriculum_prompt(role_name: str) -> str:
return (
f"You are an instructional design assistant for onboarding the role '{role_name}'. "
"Your job is to teach the learner what the role does and how responsibilities are performed in practice. "
"Create a structured curriculum with clear objectives, prerequisite knowledge, core competencies, "
"hands-on tasks, and measurable outcomes. Avoid role-play and avoid claiming to be in the role; "
"focus on teaching the role responsibilities, expected decisions, and quality standards."
)
@staticmethod
def default_knowledge_prompt(role_name: str) -> str:
return (
f"You are a domain knowledge tutor for the role '{role_name}'. "
"Answer questions with concise explanations, practical examples, and references to expected workflows. "
"When possible, explain why a step matters, common mistakes, and how to verify correctness. "
"Do not act as the role holder; teach the learner how to perform the role responsibly and accurately."
)
@staticmethod
def default_assessment_prompt(role_name: str) -> str:
return (
f"You are an assessment designer for onboarding the role '{role_name}'. "
"Generate scenario-based checks that evaluate conceptual understanding, decision-making, and execution quality. "
"Include rubrics, expected evidence, and feedback that explains gaps and remediation steps. "
"Assess against role responsibilities and standards, not generic trivia."
)
@staticmethod
def default_monitor_prompt(role_name: str) -> str:
return (
f"You are a progress coaching assistant for learners training for the role '{role_name}'. "
"Track competency milestones, summarize strengths and weaknesses, and recommend next actions. "
"Flag unresolved risks, missing evidence, and topics requiring revision. "
"Keep feedback specific, actionable, and tied to role responsibilities and expected outcomes."
)
@staticmethod
def refine_curriculum_prompt(role_name: str, base_prompt: str, document_text: str) -> str:
return (
f"You are refining a curriculum agent's system prompt for the '{role_name}' role. "
"Training documents have been uploaded. Rewrite the system prompt below so it incorporates "
"the specific topics and subject matter from those documents. "
"Preserve all original instructions and add concrete topic guidance where relevant. "
"Return ONLY the refined system prompt text — no commentary, no labels.\n\n"
f"Original system prompt:\n{base_prompt}\n\n"
f"Training document content:\n{document_text}"
)
FALLBACK_SYSTEM_PROMPT = 'You are a helpful onboarding assistant.' FALLBACK_SYSTEM_PROMPT = 'You are a helpful onboarding assistant.'
KA_HELP_FALLBACK = ( KA_HELP_FALLBACK = (

View file

@ -113,7 +113,11 @@ const currentPageBody = computed(() => {
const renderedBody = computed(() => { const renderedBody = computed(() => {
if (!currentPageBody.value) return '' if (!currentPageBody.value) return ''
const body = currentPageBody.value.replace(/^#{1,6}\s+.+\n?/, '') const lines = currentPageBody.value.split('\n')
const firstLineText = lines[0].replace(/^#{1,6}\s*/, '').trim()
const pageTitle = (currentPage.value?.title ?? '').trim()
const startsWithTitle = pageTitle && firstLineText.toLowerCase() === pageTitle.toLowerCase()
const body = startsWithTitle ? lines.slice(1).join('\n').trimStart() : currentPageBody.value
return DOMPurify.sanitize(marked.parse(body) as string) return DOMPurify.sanitize(marked.parse(body) as string)
}) })

View file

@ -969,6 +969,7 @@ onMounted(async () => {
" "
:multiple="false" :multiple="false"
:auto-upload="false" :auto-upload="false"
:file-list="[]"
> >
<p class="ant-upload-drag-icon"> <p class="ant-upload-drag-icon">
<InboxOutlined /> <InboxOutlined />
@ -1047,6 +1048,7 @@ onMounted(async () => {
" "
:multiple="false" :multiple="false"
:auto-upload="false" :auto-upload="false"
:file-list="[]"
> >
<p class="ant-upload-drag-icon"> <p class="ant-upload-drag-icon">
<InboxOutlined /> <InboxOutlined />