diff --git a/apps/onboarding/consumers.py b/apps/onboarding/consumers.py index 53d8955..5a22488 100644 --- a/apps/onboarding/consumers.py +++ b/apps/onboarding/consumers.py @@ -54,13 +54,22 @@ class OnboardingConsumer(AsyncWebsocketConsumer): await self.run_full_onboarding_generation(role_uuid) elif action == "progress_monitor": role_uuid = data.get("role_uuid") or self.context_uuid + target_user_uuid = data.get("user_uuid") + flow_uuid = data.get("flow_uuid") if not role_uuid: await self.send_log("error", "Missing role_uuid for progress monitoring") return if not await self.can_access_role(role_uuid, self.user.id): await self.send_log("error", "Forbidden") return - await self.run_progress_monitor(role_uuid) + target_user_id = self.user.id + if target_user_uuid and str(target_user_uuid) != str(self.user.uuid): + target_user_id = await self.resolve_target_user_id(role_uuid, self.user.id, target_user_uuid) + if not target_user_id: + await self.send_log("error", "Forbidden") + return + + await self.run_progress_monitor(role_uuid, target_user_id=target_user_id, flow_uuid=flow_uuid) else: user_message = data.get("query") or data.get("message") @@ -232,7 +241,7 @@ class OnboardingConsumer(AsyncWebsocketConsumer): "message": "Onboarding pipeline complete and structure saved." })) - async def run_progress_monitor(self, role_uuid): + async def run_progress_monitor(self, role_uuid, target_user_id=None, flow_uuid=None): await self.send_log("status", "Progress Monitor is analyzing your onboarding progress...", "monitor") monitor_config = await self.get_config_by_type(role_uuid, 'monitor') @@ -240,7 +249,11 @@ class OnboardingConsumer(AsyncWebsocketConsumer): await self.send_log("error", "Missing Progress Monitor AgentConfig for this role") return - progress_context = await self.get_role_progress_context(role_uuid, self.user.id) + progress_context = await self.get_role_progress_context( + role_uuid, + target_user_id or self.user.id, + flow_uuid=flow_uuid, + ) monitor_prompt = ( "You are a progress monitoring agent for onboarding. " @@ -250,12 +263,21 @@ class OnboardingConsumer(AsyncWebsocketConsumer): f"Progress context JSON:\n{json.dumps(progress_context)}" ) - feedback = await self.orchestrate_ai( - monitor_prompt, - monitor_config, - min_internal_turns=1, - max_tokens=640, - ) + try: + feedback = await self.orchestrate_ai( + monitor_prompt, + monitor_config, + min_internal_turns=1, + max_tokens=640, + raise_on_error=True, + ) + except Exception as exc: + await self.send_log("error", f"Inference failed: {str(exc)}") + return + + if str(feedback).startswith("Error:"): + await self.send_log("error", str(feedback)) + return await self.send(json.dumps({ "type": "completed", @@ -265,6 +287,9 @@ class OnboardingConsumer(AsyncWebsocketConsumer): "role_uuid": role_uuid, "feedback": feedback, "status": progress_context.get("latest_status", "unknown"), + "user_id": target_user_id or self.user.id, + "flow_uuid": flow_uuid, + "is_completed": progress_context.get("is_completed", False), } })) @@ -275,6 +300,7 @@ class OnboardingConsumer(AsyncWebsocketConsumer): min_internal_turns=2, max_turns=6, max_tokens=None, + raise_on_error=False, ): """ Handles the multi-turn ReAct loop (Reasoning + Tool Use). @@ -360,6 +386,8 @@ class OnboardingConsumer(AsyncWebsocketConsumer): except Exception as e: await self.send_log("error", f"Inference failed: {str(e)}") + if raise_on_error: + raise return f"Error: {str(e)}" return last_content @@ -663,13 +691,20 @@ class OnboardingConsumer(AsyncWebsocketConsumer): ).order_by('-updated_at').first() @database_sync_to_async - def get_role_progress_context(self, role_uuid, user_id): + def get_role_progress_context(self, role_uuid, user_id, flow_uuid=None): from apps.accounts.models import Role role = Role.objects.get(uuid=role_uuid) - sessions = OnboardingSession.objects.filter(user_id=user_id, role=role).order_by('-updated_at') - latest_session = sessions.first() active_flow = OnboardingFlow.objects.filter(role=role, is_active=True).order_by('-updated_at').first() + scoped_flow = None + if flow_uuid: + scoped_flow = OnboardingFlow.objects.filter(role=role, uuid=flow_uuid).first() + + sessions = OnboardingSession.objects.filter(user_id=user_id, role=role).order_by('-updated_at') + if flow_uuid: + sessions = sessions.filter(state__flow_uuid=str(flow_uuid)) + + latest_session = sessions.first() if not latest_session: return { @@ -677,10 +712,12 @@ class OnboardingConsumer(AsyncWebsocketConsumer): "role_name": role.name, "latest_status": "not_started", "session_count": 0, - "flow_exists": bool(active_flow), + "flow_exists": bool(scoped_flow or active_flow), + "flow_uuid": str((scoped_flow or active_flow).uuid) if (scoped_flow or active_flow) else None, "progress": 0, "responses_count": 0, "completed_modules": [], + "is_completed": False, } state = latest_session.state or {} @@ -693,9 +730,31 @@ class OnboardingConsumer(AsyncWebsocketConsumer): "role_name": role.name, "latest_status": latest_session.status, "session_count": sessions.count(), - "flow_exists": bool(active_flow), + "flow_exists": bool(scoped_flow or active_flow), + "flow_uuid": str((scoped_flow or active_flow).uuid) if (scoped_flow or active_flow) else None, "progress": progress, "responses_count": len(responses) if isinstance(responses, dict) else 0, "completed_modules": completed_modules if isinstance(completed_modules, list) else [], "updated_at": latest_session.updated_at.isoformat() if latest_session.updated_at else None, - } \ No newline at end of file + "is_completed": latest_session.status == 'completed', + } + + @database_sync_to_async + def resolve_target_user_id(self, role_uuid, requester_id, target_user_uuid): + from apps.accounts.models import Role, User + + role = Role.objects.filter(uuid=role_uuid).first() + requester = User.objects.filter(id=requester_id).first() + target = User.objects.filter(uuid=target_user_uuid).first() + if role is None or requester is None or target is None: + return None + + is_owner = role.organization.owner.id == requester_id + is_manager_member = bool(requester.is_manager) and role.organization.members.filter(id=requester_id).exists() + if not (is_owner or is_manager_member): + return None + + if not role.members.filter(id=target.id).exists(): + return None + + return target.id \ No newline at end of file diff --git a/site/src/router/api.ts b/site/src/router/api.ts index dd258fc..b1192ab 100644 --- a/site/src/router/api.ts +++ b/site/src/router/api.ts @@ -144,6 +144,7 @@ export const API = { }, sessions: { list: () => 'onboarding-session/', + progressOverview: () => 'onboarding-session/progress-overview/', byId: (uuid: string) => `onboarding-session/${uuid}/`, interact: (uuid: string) => `onboarding-session/${uuid}/interact/`, askKa: (uuid: string) => `onboarding-session/${uuid}/ask-ka/`, diff --git a/site/src/types/onboarding.ts b/site/src/types/onboarding.ts index c94cc84..47280e2 100644 --- a/site/src/types/onboarding.ts +++ b/site/src/types/onboarding.ts @@ -29,7 +29,6 @@ export type OnboardingFlow = { agent?: string | null title: string description?: string - status: 'draft' | 'published' | 'archived' pages?: OnboardingPage[] } @@ -63,6 +62,12 @@ export type ProgressSessionApi = { uuid: string status: OnboardingSessionStatus role: UuidNameRef + user?: { + uuid: string + email_address?: string + first_name?: string + last_name?: string + } updated_at?: string state?: Record } @@ -81,3 +86,26 @@ export type RoleProgressItem = { feedback?: string loadingFeedback: boolean } + +export type ProgressOverviewItem = { + role: UuidNameRef + user: { + uuid: string + name: string + email: string + } + flow: { + uuid: string + title: string + is_active: boolean + } + latest_status: string + progress: number + is_completed: boolean + latest_session_uuid?: string | null + updated_at?: string | null +} + +export type FlowLookup = { + title?: string +} diff --git a/site/src/types/organization.ts b/site/src/types/organization.ts index a58ec36..fedfa3f 100644 --- a/site/src/types/organization.ts +++ b/site/src/types/organization.ts @@ -16,6 +16,7 @@ export interface Role { name: string description?: string organization: Organization + members?: User[] member_count?: number created_at: string updated_at: string diff --git a/site/src/views/ProgressDetailView.vue b/site/src/views/ProgressDetailView.vue index 64ddf4f..7e939e8 100644 --- a/site/src/views/ProgressDetailView.vue +++ b/site/src/views/ProgressDetailView.vue @@ -1,21 +1,26 @@