From e19f2f494fdc06ca1c25549eaca8495286d53522 Mon Sep 17 00:00:00 2001 From: Bereket Terefe Date: Thu, 9 Jul 2026 17:35:06 +0300 Subject: [PATCH 1/2] fix(recommender): implement dynamic batch size for recommendations and add show more functionality [pulumi up] --- .../intent_classifier.py | 43 ++-- .../llm_response_models.py | 2 +- .../phase_handlers/present_handler.py | 187 +++++++++++++++++- backend/app/app_config.py | 9 + backend/app/server.py | 4 + iac/backend/__main__.py | 3 + iac/backend/deploy_backend.py | 4 + iac/templates/env.template | 4 + 8 files changed, 238 insertions(+), 18 deletions(-) diff --git a/backend/app/agent/recommender_advisor_agent/intent_classifier.py b/backend/app/agent/recommender_advisor_agent/intent_classifier.py index 22624e52e..eeef9dda0 100644 --- a/backend/app/agent/recommender_advisor_agent/intent_classifier.py +++ b/backend/app/agent/recommender_advisor_agent/intent_classifier.py @@ -10,6 +10,7 @@ from app.agent.agent_types import LLMStats from app.agent.llm_caller import LLMCaller +from app.app_config import get_application_config from app.agent.recommender_advisor_agent.state import RecommenderAdvisorAgentState from app.agent.recommender_advisor_agent.types import ConversationPhase from app.agent.recommender_advisor_agent.llm_response_models import UserIntentClassification @@ -18,6 +19,16 @@ from common_libs.llm.generative_models import GeminiGenerativeLLM +def _batch_size() -> int: + """Configured recommendation batch size, with a safe default when the + application config has not been initialised (e.g. unit tests that construct + the classifier directly without going through the application setup).""" + try: + return get_application_config().recommendation_batch_size + except RuntimeError: + return 5 + + class IntentClassifier: """ Centralized intent classification for the Recommender/Advisor Agent. @@ -164,14 +175,14 @@ def _build_jobs_followup_phase_prompt(self, user_input: str, state: RecommenderA """ opp_list = "" if state.recommendations: - for i, opp in enumerate(state.recommendations.opportunity_recommendations[:5], 1): + for i, opp in enumerate(state.recommendations.opportunity_recommendations[:_batch_size()], 1): opp_list += f"{i}. {opp.opportunity_title} (uuid: {opp.uuid})\n" # The "both" view also showed career paths, so the user might reference one of those. occ_block = "" if state.recommendation_view == "both" and state.recommendations: occ_lines = "" - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): occ_lines += f"{i}. {occ.occupation} (uuid: {occ.uuid})\n" if occ_lines: occ_block = f""" @@ -196,6 +207,10 @@ def _build_jobs_followup_phase_prompt(self, user_input: str, state: RecommenderA (its NUMBER in the job-openings list above), using case-insensitive / partial matching. - "show_careers": They want to see career paths instead of (or in addition to) the openings (e.g. "show me career paths", "what careers do you suggest?"). +- "show_more": They want MORE job openings (or, in the "both" view, more of the same recommendation type) + without naming a specific opening. Examples: "show me more jobs", "any other openings?", + "what else do you have?", "next", "keep going", "more", "nyingine". + If they name a SPECIFIC opening above, that is EXPLORE_OPPORTUNITY (not show_more). - "express_concern": They're expressing worry, doubt, or an objection about an opening. - "ask_question": A factual question about an opening (e.g. "where is it?", "is it remote?"). - "accept": They want to move forward / apply. @@ -204,7 +219,7 @@ def _build_jobs_followup_phase_prompt(self, user_input: str, state: RecommenderA Your response must be a JSON object with the following schema: {{ "reasoning": "A step by step explanation of why you classified this intent", - "intent": "One of: explore_opportunity, explore_occupation, show_careers, express_concern, ask_question, accept, other", + "intent": "One of: explore_opportunity, explore_occupation, show_careers, show_more, express_concern, ask_question, accept, other", "target_recommendation_id": "The UUID of the opening (or career path) if identified, or null", "target_occupation_index": "The 1-based index number in its list if identified, or null", "requested_occupation_name": null @@ -227,7 +242,7 @@ def _build_present_phase_prompt(self, user_input: str, state: RecommenderAdvisor # Build occupation list for reference occ_list = "" if state.recommendations: - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): occ_list += f"{i}. {occ.occupation} (uuid: {occ.uuid})\n" return f""" @@ -262,7 +277,13 @@ def _build_present_phase_prompt(self, user_input: str, state: RecommenderAdvisor Possible intents: - "explore_occupation": They want to learn more about a specific occupation (e.g., "tell me more about Data Analyst", "I'm interested in #1", "what would I do day-to-day?", "I love the marine role") CRITICAL: When this intent is detected, you MUST also extract target_occupation_index AND target_recommendation_id using the matching rules above +- "show_more": They want to see MORE / DIFFERENT recommendations from the same category, without naming a specific one. + Examples: "show me more", "any others?", "what else?", "more options", "next", "keep going", "see more", "any other careers?", "any other jobs?", "different ones", "show me the next batch", "nyingine", "zaidi" + CRITICAL: They are asking for the NEXT page of the same recommendation type — not naming a specific item. + If they name a specific occupation from the list, that is EXPLORE_OCCUPATION (not show_more). + If they name an occupation NOT in the list, that is REQUEST_OUTSIDE_RECOMMENDATIONS. - "reject": They're explicitly rejecting the current recommendations (e.g., "not interested", "I don't like these", "none of these appeal to me", "I reject that") + IMPORTANT: "not these ones, show me others" is SHOW_MORE (wants next batch), not REJECT. - "express_concern": They're expressing worry, doubt, hesitation, or identifying problems with a recommendation Examples: "I'm worried about...", "what if...", "but...", "seems like it would...", "this takes too much...", "I don't think I can...", "sounds too...", "not enough...", "too boring", "doesn't sound adventurous" Key: ANY statement that raises an objection, barrier, or negative aspect about a recommendation @@ -286,7 +307,7 @@ def _build_present_phase_prompt(self, user_input: str, state: RecommenderAdvisor Your response must be a JSON object with the following schema: {{ "reasoning": "A step by step explanation of why you classified this intent", - "intent": "One of: explore_occupation, reject, express_concern, ask_question, accept, request_outside_recommendations, other", + "intent": "One of: explore_occupation, show_more, reject, express_concern, ask_question, accept, request_outside_recommendations, other", "target_recommendation_id": "The UUID of the recommendation if identified, or null", "target_occupation_index": "The 1-based index number if identified, or null", "requested_occupation_name": "The occupation name if they requested something outside recommendations, or null" @@ -315,7 +336,7 @@ def _build_exploration_phase_prompt(self, user_input: str, state: RecommenderAdv # Build list of other available occupations other_occs = "" if state.recommendations: - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): if occ.uuid != state.current_focus_id: other_occs += f"{i}. {occ.occupation} (uuid: {occ.uuid})\n" @@ -412,7 +433,7 @@ def _build_followup_phase_prompt(self, user_input: str, state: RecommenderAdviso # Build occupation list for reference occ_list = "" if state.recommendations: - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): occ_list += f"{i}. {occ.occupation} (uuid: {occ.uuid})\n" return f""" @@ -469,7 +490,7 @@ def _build_concerns_phase_prompt(self, user_input: str, state: RecommenderAdviso # Build list of other available occupations other_occs = "" if state.recommendations: - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): other_occs += f"{i}. {occ.occupation} (uuid: {occ.uuid})\n" return f""" @@ -556,13 +577,13 @@ def _build_skills_pivot_phase_prompt(self, user_input: str, state: RecommenderAd # Build training list trainings_list = "" if state.recommendations and state.recommendations.skillstraining_recommendations: - for i, trn in enumerate(state.recommendations.skillstraining_recommendations[:5], 1): + for i, trn in enumerate(state.recommendations.skillstraining_recommendations[:_batch_size()], 1): trainings_list += f"{i}. {trn.training_title} - {trn.skill} (uuid: {trn.uuid})\n" # Build occupations list for going back occs_list = "" if state.recommendations and state.recommendations.occupation_recommendations: - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): occs_list += f"{i}. {occ.occupation} (uuid: {occ.uuid})\n" return f""" @@ -647,7 +668,7 @@ def _build_generic_prompt(self, user_input: str, state: RecommenderAdvisorAgentS # Build occupation list if available occ_list = "" if state.recommendations: - for i, occ in enumerate(state.recommendations.occupation_recommendations[:5], 1): + for i, occ in enumerate(state.recommendations.occupation_recommendations[:_batch_size()], 1): occ_list += f"{i}. {occ.occupation}\n" occ_context = f""" diff --git a/backend/app/agent/recommender_advisor_agent/llm_response_models.py b/backend/app/agent/recommender_advisor_agent/llm_response_models.py index df7e61471..29589eb8e 100644 --- a/backend/app/agent/recommender_advisor_agent/llm_response_models.py +++ b/backend/app/agent/recommender_advisor_agent/llm_response_models.py @@ -105,7 +105,7 @@ class UserIntentClassification(BaseModel): description="Reasoning about what the user wants to do" ) intent: str = Field( - description="User intent: 'explore_occupation', 'show_opportunities', 'express_concern', 'ask_question', 'reject', 'accept', 'discuss_next_steps', 'explore_alternatives', 'address_more_concerns', 'other'" + description="User intent: 'explore_occupation', 'show_opportunities', 'show_more', 'express_concern', 'ask_question', 'reject', 'accept', 'discuss_next_steps', 'explore_alternatives', 'address_more_concerns', 'other'" ) target_recommendation_id: Optional[str] = Field( default=None, diff --git a/backend/app/agent/recommender_advisor_agent/phase_handlers/present_handler.py b/backend/app/agent/recommender_advisor_agent/phase_handlers/present_handler.py index d232d27f3..9da4f36e0 100644 --- a/backend/app/agent/recommender_advisor_agent/phase_handlers/present_handler.py +++ b/backend/app/agent/recommender_advisor_agent/phase_handlers/present_handler.py @@ -25,7 +25,7 @@ PRESENT_RECOMMENDATIONS_PROMPT, build_context_block ) -from app.agent.recommender_advisor_agent.intent_classifier import IntentClassifier +from app.agent.recommender_advisor_agent.intent_classifier import IntentClassifier, _batch_size from app.conversation_memory.conversation_memory_manager import ConversationContext from app.conversation_memory.conversation_formatter import ConversationHistoryFormatter from app.agent.simple_llm_agent.prompt_response_template import get_json_response_instructions @@ -178,11 +178,11 @@ async def handle( return response, all_llm_stats # Get top occupations to present (strict rank order) - occupations = state.recommendations.occupation_recommendations[:5] + occupations = state.recommendations.occupation_recommendations[:_batch_size()] if not occupations: # Fall back to opportunity recommendations if no occupations are available - opportunities = state.recommendations.opportunity_recommendations[:5] + opportunities = state.recommendations.opportunity_recommendations[:_batch_size()] if opportunities: self.logger.info( f"No occupation recommendations available; presenting {len(opportunities)} opportunity recommendations instead" @@ -379,7 +379,7 @@ async def _present_jobs_view( context: ConversationContext, ) -> tuple[ConversationResponse, list[LLMStats]]: """Present job openings, keeping the conversation open (jobs-only view).""" - opportunities = state.recommendations.opportunity_recommendations[:5] + opportunities = state.recommendations.opportunity_recommendations[:_batch_size()] if not opportunities: return ConversationResponse( reasoning="User chose jobs but no job openings are available", @@ -407,8 +407,8 @@ async def _present_both_view( context: ConversationContext, ) -> tuple[ConversationResponse, list[LLMStats]]: """Present job openings first, then career paths, in one message (both view).""" - opportunities = state.recommendations.opportunity_recommendations[:5] - occupations = state.recommendations.occupation_recommendations[:5] + opportunities = state.recommendations.opportunity_recommendations[:_batch_size()] + occupations = state.recommendations.occupation_recommendations[:_batch_size()] if not opportunities and not occupations: return ConversationResponse( @@ -550,6 +550,10 @@ async def _handle_user_intent( elif intent.intent == "accept": return await self._handle_accept_intent(intent, user_input, state, context) + # Handle SHOW_MORE intent - user wants the next batch of recommendations + elif intent.intent == "show_more": + return await self._handle_show_more_intent(user_input, state, context) + # For other intents (questions, unclear), let LLM handle conversationally # The LLM has instructions in the base prompt to handle user-suggested occupations return None @@ -624,6 +628,172 @@ async def _handle_explore_intent( self.logger.warning(f"Could not identify target occupation from intent. target_occupation_index={intent.target_occupation_index}, target_recommendation_id={intent.target_recommendation_id}") return None + @staticmethod + def _next_unpresented( + all_recs: list, presented_ids: list[str], k: Optional[int] = None + ) -> list: + """Next ``k`` recommendations in rank order that the user has not seen yet. + + ``presented_ids`` is treated as an ordered log (initial batch, then any prior + show_more batches), so callers can compute both what to render next and + whether the underlying list is exhausted. ``k`` defaults to the configured + recommendation batch size (``COMPASS_RECOMMENDATION_BATCH_SIZE``). + """ + seen = set(presented_ids) + limit = _batch_size() if k is None else k + return [r for r in all_recs if r.uuid not in seen][:limit] + + async def _handle_show_more_intent( + self, + user_input: str, + state: RecommenderAdvisorAgentState, + context: ConversationContext, + ) -> tuple[ConversationResponse, list[LLMStats]]: + """Present the next batch of recommendations in the user's current view. + + - ``careers`` view (or default) → next 5 occupations. + - ``jobs`` view → next 5 opportunities. + - ``both`` view → next 5 opportunities followed by next 5 occupations. + + When the underlying list is exhausted, returns a graceful "that's all" + message that invites the user to go deeper on one of the ones already shown + (rather than looping back to page 1). Stays in PRESENT_RECOMMENDATIONS. + """ + if state.recommendations is None: + return ConversationResponse( + reasoning="show_more requested but no recommendations are cached", + message="Let me pull up your recommendations first — say the word when you're ready.", + finished=False, + ), [] + + view = state.recommendation_view or "careers" + all_llm_stats: list[LLMStats] = [] + + if view == "jobs": + next_opps = self._next_unpresented( + state.recommendations.opportunity_recommendations, + state.presented_opportunities, + ) + if not next_opps: + return self._exhausted_response("opportunity", state), [] + for opp in next_opps: + state.presented_opportunities.append(opp.uuid) + recs_summary = self._build_opportunities_summary(next_opps) + full_prompt = self._build_view_prompt(state, context, recs_summary, _PRESENT_JOBS_PROMPT) + response, llm_stats = await self._call_llm(full_prompt, user_input, context) + response.finished = False + all_llm_stats.extend(llm_stats) + return response, all_llm_stats + + if view == "both": + next_opps = self._next_unpresented( + state.recommendations.opportunity_recommendations, + state.presented_opportunities, + ) + next_occs = self._next_unpresented( + state.recommendations.occupation_recommendations, + state.presented_occupations, + ) + if not next_opps and not next_occs: + return self._exhausted_response("both", state), [] + for opp in next_opps: + state.presented_opportunities.append(opp.uuid) + for occ in next_occs: + state.presented_occupations.append(occ.uuid) + jobs_block = (self._build_opportunities_summary(next_opps) + if next_opps else "No more job openings to show.") + careers_block = (self._build_detailed_recommendations_summary(next_occs) + if next_occs else "No more career paths to show.") + recs_summary = ( + f"{jobs_block}\n\n" + f"**Career Paths** (present these after the jobs):\n{careers_block}" + ) + full_prompt = self._build_view_prompt(state, context, recs_summary, _PRESENT_BOTH_PROMPT) + response, llm_stats = await self._call_llm(full_prompt, user_input, context) + response.finished = False + all_llm_stats.extend(llm_stats) + return response, all_llm_stats + + # Default: careers view. + next_occs = self._next_unpresented( + state.recommendations.occupation_recommendations, + state.presented_occupations, + ) + if not next_occs: + return self._exhausted_response("occupation", state), [] + for occ in next_occs: + state.presented_occupations.append(occ.uuid) + recs_summary = self._build_detailed_recommendations_summary(next_occs) + skills_list = self._extract_skills_list(state) + pref_vec_dict = state.preference_vector.model_dump() if state.preference_vector else {} + conv_history = ConversationHistoryFormatter.format_to_string(context) + context_block = build_context_block( + skills=skills_list, + preference_vector=pref_vec_dict, + recommendations_summary=recs_summary, + conversation_history=conv_history, + country_of_user=state.country_of_user, + ) + full_prompt = context_block + PRESENT_RECOMMENDATIONS_PROMPT + get_json_response_instructions() + response, llm_stats = await self._call_llm(full_prompt, user_input, context) + response.finished = False + all_llm_stats.extend(llm_stats) + + response.metadata = self._build_metadata( + interaction_type="occupation_presentation", + occupations=[ + { + "uuid": occ.uuid, + "occupation": occ.occupation, + "rank": occ.rank, + "confidence_score": occ.confidence_score, + "labor_demand_category": occ.labor_demand_category, + "salary_range": occ.salary_range, + "justification": occ.justification, + "skills_match_score": occ.skills_match_score, + "preference_match_score": occ.preference_match_score, + "labor_demand_score": occ.labor_demand_score, + } + for occ in next_occs + ], + ) + return response, all_llm_stats + + def _exhausted_response( + self, + kind: str, + state: RecommenderAdvisorAgentState, + ) -> ConversationResponse: + """Reply when the user has already seen every available recommendation of a kind. + + We deliberately do not loop back to page 1 — surfacing repeats without saying so + would erode trust. Instead, invite the user to go deeper on something already shown + (or, in the jobs case, to check back later for new listings).""" + if kind == "opportunity": + message = ( + "Those are all the job openings I have for you right now. New listings come in " + "regularly, so it's worth checking back another day. In the meantime, would you " + "like to dig into any of the openings we've already looked at, or switch to career " + "paths to consider?" + ) + elif kind == "both": + message = ( + "That's everything I have on both sides for now — jobs and career paths. New " + "openings come in regularly, so check back another day. For now, would you like " + "to go deeper on any of the options we've already looked at?" + ) + else: + message = ( + "Those are all the career paths I have that fit your profile. Would you like to " + "go deeper on any of the ones we've already looked at, or shall we look at actual " + "job openings you could apply to?" + ) + return ConversationResponse( + reasoning=f"No more {kind} recommendations to present; offering to explore shown items", + message=message, + finished=False, + ) + async def _handle_jobs_followup( self, @@ -670,6 +840,11 @@ async def _handle_jobs_followup( state.recommendation_view = "careers" return None + elif intent.intent == "show_more": + # Paginate the next batch in the same view (jobs, or jobs+careers for "both"). + response, s = await self._handle_show_more_intent(user_input, state, context) + return response, all_llm_stats + s + elif intent.intent == "express_concern": response, s = await self._handle_concern_intent(intent, user_input, state, context) return response, all_llm_stats + s diff --git a/backend/app/app_config.py b/backend/app/app_config.py index 144581b8c..f05824e36 100644 --- a/backend/app/app_config.py +++ b/backend/app/app_config.py @@ -109,6 +109,15 @@ class ApplicationConfig(BaseModel): Corresponds to the COMPASS_INLINE_PHASE_TRANSITION environment variable. """ + recommendation_batch_size: int = Field(default=5, ge=1, le=50) + """ + Number of recommendations (occupations or job openings) presented per batch by the + Recommender/Advisor Agent — both in the initial presentation and in each + subsequent "show me more" turn. Also caps the list of options shown to the + intent classifier when it disambiguates which item the user is referring to. + Corresponds to the COMPASS_RECOMMENDATION_BATCH_SIZE environment variable. + """ + @model_validator(mode='after') def check_cv_upload_configurations(self) -> "ApplicationConfig": # Check that the CV upload configurations are valid. diff --git a/backend/app/server.py b/backend/app/server.py index ce5857d11..da92e73a6 100644 --- a/backend/app/server.py +++ b/backend/app/server.py @@ -234,6 +234,10 @@ def setup_sentry(): matching_service_url=os.getenv("MATCHING_SERVICE_URL"), matching_service_api_key=os.getenv("MATCHING_SERVICE_API_KEY"), inline_phase_transition=os.getenv("COMPASS_INLINE_PHASE_TRANSITION", "").lower() in ("1", "true"), + **( + {"recommendation_batch_size": int(os.environ["COMPASS_RECOMMENDATION_BATCH_SIZE"])} + if os.getenv("COMPASS_RECOMMENDATION_BATCH_SIZE") else {} + ), ) set_application_config(application_config) diff --git a/iac/backend/__main__.py b/iac/backend/__main__.py index b640749e6..1ea7d10a9 100644 --- a/iac/backend/__main__.py +++ b/iac/backend/__main__.py @@ -94,6 +94,9 @@ def main(): # Phase transition behavior (optional) inline_phase_transition=getenv("COMPASS_INLINE_PHASE_TRANSITION", False, False), + + # Recommender/Advisor batch size (optional; defaults to 5 in the backend) + recommendation_batch_size=getenv("COMPASS_RECOMMENDATION_BATCH_SIZE", False, False), ) # version of the artifacts to deploy diff --git a/iac/backend/deploy_backend.py b/iac/backend/deploy_backend.py index d2185691e..d7a74ab82 100644 --- a/iac/backend/deploy_backend.py +++ b/iac/backend/deploy_backend.py @@ -61,6 +61,7 @@ class BackendServiceConfig: matching_service_url: Optional[str] matching_service_api_key: Optional[str] inline_phase_transition: Optional[str] + recommendation_batch_size: Optional[str] """ @@ -436,6 +437,9 @@ def _deploy_cloud_run_service( gcp.cloudrunv2.ServiceTemplateContainerEnvArgs( name="COMPASS_INLINE_PHASE_TRANSITION", value=backend_service_cfg.inline_phase_transition), + gcp.cloudrunv2.ServiceTemplateContainerEnvArgs( + name="COMPASS_RECOMMENDATION_BATCH_SIZE", + value=backend_service_cfg.recommendation_batch_size), # Add more environment variables here ], ) diff --git a/iac/templates/env.template b/iac/templates/env.template index 3854c5db6..2da9e5ef5 100644 --- a/iac/templates/env.template +++ b/iac/templates/env.template @@ -92,6 +92,10 @@ FRONTEND_FEATURES=/.*/ MATCHING_SERVICE_URL=/.*/ MATCHING_SERVICE_API_KEY=/.*/ +# Recommender/Advisor: recommendations presented per batch (initial + each "show more"). +# Also caps the list of options the intent classifier sees. Optional; defaults to 5. +COMPASS_RECOMMENDATION_BATCH_SIZE=/^([1-9]|[1-4][0-9]|50)$/ + BACKEND_LANGUAGE_CONFIG={"default_locale":"","available_locales":[{"locale":"","date_format":""}]} FRONTEND_DEFAULT_LOCALE= FRONTEND_SUPPORTED_LOCALES=["",""] From 90b62827c49fe9bd3ccf84a31005813e72bb1586 Mon Sep 17 00:00:00 2001 From: Bereket Terefe Date: Fri, 10 Jul 2026 09:47:11 +0300 Subject: [PATCH 2/2] feat(messages): update welcome message to include improved Swahili translation and formatting --- backend/app/i18n/locales/en-GB/messages.json | 2 +- backend/app/i18n/locales/en-US/messages.json | 2 +- backend/app/i18n/locales/sw-KE/messages.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/i18n/locales/en-GB/messages.json b/backend/app/i18n/locales/en-GB/messages.json index ab8597220..b94b85c73 100644 --- a/backend/app/i18n/locales/en-GB/messages.json +++ b/backend/app/i18n/locales/en-GB/messages.json @@ -1,5 +1,5 @@ { - "welcomeAgentFirstEncounter": "Welcome to {app_name}! I’m your digital mentor, here to help you map out your skills and discover the best career or job opportunities for you \uD83D\uDE42.\n\nHere is how we will do it:\n\n• Share your experience: We'll chat about your past work, informal gigs, tasks you do at home, and volunteering. You can also upload your CV to give us a head start!\n\n• Discover your preferences: I'll show you a few short career scenarios to help figure out what matters most to you in a job, and what tasks you enjoy doing.\n\n• Get matched: Finally, I'll recommend concrete career paths and jobs that fit your unique profile.\n\nYou can chat with me in English or Swahili, and don't worry about being too formal -- just use your own words!\n\nYour profile and CV automatically saves as we go, so if you need to step away, you can always pick up right where you left off. You can also always come back and download or edit your CV, even after our conversation ends!\n\nReady to get started?", + "welcomeAgentFirstEncounter": "Karibu kwenye {app_name}! Mimi ni mwelekezi wako wa kidijitali, niko hapa kukusaidia kutambua ujuzi wako na kugundua nafasi bora za kazi au taaluma zinazokufaa 🙂.\n\nHivi ndivyo tutakavyofanya:\n\n- **Eleza ujuzi wako:** Tutazungumza kuhusu kazi zako za awali, vibarua, kazi za nyumbani, na kazi za kujitolea. Pia unaweza kupakia (upload) CV yako ili kuturahisishia kazi!\n- **Gundua vitu unavyopendelea:** Nitakuonyesha mifano mifupi ya mazingira ya kazi ili kusaidia kujua ni nini muhimu zaidi kwako katika kazi, na ni majukumu gani unapenda kufanya.\n- **Pata mapendekezo:** Mwishowe, nitakupendekezea njia za kukuza taaluma na kazi mahususi zinazolingana na wasifu wako.\n\n**Unaweza kuwasiliana nami kwa Kiswahili, au Kiingereza (English works perfectly too!).** Usijali kuhusu kutumia lugha rasmi sana — tumia tu maneno yako ya kawaida, au hata kuchanganya lugha!\n\nWasifu na CV yako inajihifadhi yenyewe (saves automatically) tunapoendelea, kwa hivyo ukihitaji kupumzika kidogo, unaweza kurudi na kuendelea pale ulipoishia. Pia unaweza kurudi wakati wowote kupakua (download) au kurekebisha CV yako, hata baada ya mazungumzo yetu kuisha!\n\n**Uko tayari kuanza? / Ready to get started?** 🚀\nIli kuanza, andika tu \"Niko tayari\" au \"Twende!\" kisha utume.\n*(To begin, just type \"I am ready\" or \"Let's go!\" and hit send!)*", "collectExperiences": { "didNotUnderstand": "Sorry, I didn't understand that. Can you please rephrase?", "moveToOtherExperiences": "Let's move on to other work experiences.", diff --git a/backend/app/i18n/locales/en-US/messages.json b/backend/app/i18n/locales/en-US/messages.json index ab8597220..b94b85c73 100644 --- a/backend/app/i18n/locales/en-US/messages.json +++ b/backend/app/i18n/locales/en-US/messages.json @@ -1,5 +1,5 @@ { - "welcomeAgentFirstEncounter": "Welcome to {app_name}! I’m your digital mentor, here to help you map out your skills and discover the best career or job opportunities for you \uD83D\uDE42.\n\nHere is how we will do it:\n\n• Share your experience: We'll chat about your past work, informal gigs, tasks you do at home, and volunteering. You can also upload your CV to give us a head start!\n\n• Discover your preferences: I'll show you a few short career scenarios to help figure out what matters most to you in a job, and what tasks you enjoy doing.\n\n• Get matched: Finally, I'll recommend concrete career paths and jobs that fit your unique profile.\n\nYou can chat with me in English or Swahili, and don't worry about being too formal -- just use your own words!\n\nYour profile and CV automatically saves as we go, so if you need to step away, you can always pick up right where you left off. You can also always come back and download or edit your CV, even after our conversation ends!\n\nReady to get started?", + "welcomeAgentFirstEncounter": "Karibu kwenye {app_name}! Mimi ni mwelekezi wako wa kidijitali, niko hapa kukusaidia kutambua ujuzi wako na kugundua nafasi bora za kazi au taaluma zinazokufaa 🙂.\n\nHivi ndivyo tutakavyofanya:\n\n- **Eleza ujuzi wako:** Tutazungumza kuhusu kazi zako za awali, vibarua, kazi za nyumbani, na kazi za kujitolea. Pia unaweza kupakia (upload) CV yako ili kuturahisishia kazi!\n- **Gundua vitu unavyopendelea:** Nitakuonyesha mifano mifupi ya mazingira ya kazi ili kusaidia kujua ni nini muhimu zaidi kwako katika kazi, na ni majukumu gani unapenda kufanya.\n- **Pata mapendekezo:** Mwishowe, nitakupendekezea njia za kukuza taaluma na kazi mahususi zinazolingana na wasifu wako.\n\n**Unaweza kuwasiliana nami kwa Kiswahili, au Kiingereza (English works perfectly too!).** Usijali kuhusu kutumia lugha rasmi sana — tumia tu maneno yako ya kawaida, au hata kuchanganya lugha!\n\nWasifu na CV yako inajihifadhi yenyewe (saves automatically) tunapoendelea, kwa hivyo ukihitaji kupumzika kidogo, unaweza kurudi na kuendelea pale ulipoishia. Pia unaweza kurudi wakati wowote kupakua (download) au kurekebisha CV yako, hata baada ya mazungumzo yetu kuisha!\n\n**Uko tayari kuanza? / Ready to get started?** 🚀\nIli kuanza, andika tu \"Niko tayari\" au \"Twende!\" kisha utume.\n*(To begin, just type \"I am ready\" or \"Let's go!\" and hit send!)*", "collectExperiences": { "didNotUnderstand": "Sorry, I didn't understand that. Can you please rephrase?", "moveToOtherExperiences": "Let's move on to other work experiences.", diff --git a/backend/app/i18n/locales/sw-KE/messages.json b/backend/app/i18n/locales/sw-KE/messages.json index ca5715c53..4f6c2f776 100644 --- a/backend/app/i18n/locales/sw-KE/messages.json +++ b/backend/app/i18n/locales/sw-KE/messages.json @@ -1,5 +1,5 @@ { - "welcomeAgentFirstEncounter": "Karibu kwenye {app_name}! Mimi ni mshauri wako wa kidijitali, niko hapa kukusaidia kupanga ujuzi wako na kugundua fursa bora za kazi au ajira kwa ajili yako \uD83D\uDE42.\n\nHivi ndivyo tutakavyofanya:\n\nShiriki uzoefu wako: Tutazungumza kuhusu kazi zako za awali, vibarua, majukumu unayofanya nyumbani, na kazi za kujitolea. Pia unaweza kupakia (upload) CV yako ili kutupa mwanzo mzuri!\n\nGundua mapendeleo yako: Nitakuonyesha mifano michache ya kazi ili kusaidia kubaini ni kitu gani muhimu zaidi kwako katika kazi, na ni majukumu gani unayofurahia kufanya.\n\nPata mapendekezo: Hatimaye, nitakupendekezia njia madhubuti za kikazi na ajira zinazolingana na wasifu wako wa kipekee.\n\nUnaweza kuzungumza nami kwa Kiingereza au Kiswahili, na wala usijali kuhusu kutumia lugha rasmi — tumia tu maneno yako ya kawaida!\n\nWasifu wako na CV huhifadhiwa moja kwa moja tunapoendelea, kwa hivyo ukihitaji kuondoka kidogo, unaweza kurejea na kuendelea pale ulipoishia. Pia, unaweza kurudi wakati wowote kupakua au kuhariri CV yako, hata baada ya mazungumzo yetu kuisha!\n\nUko tayari kuanza?", + "welcomeAgentFirstEncounter": "Karibu kwenye {app_name}! Mimi ni mwelekezi wako wa kidijitali, niko hapa kukusaidia kutambua ujuzi wako na kugundua nafasi bora za kazi au taaluma zinazokufaa 🙂.\n\nHivi ndivyo tutakavyofanya:\n\n- **Eleza ujuzi wako:** Tutazungumza kuhusu kazi zako za awali, vibarua, kazi za nyumbani, na kazi za kujitolea. Pia unaweza kupakia (upload) CV yako ili kuturahisishia kazi!\n- **Gundua vitu unavyopendelea:** Nitakuonyesha mifano mifupi ya mazingira ya kazi ili kusaidia kujua ni nini muhimu zaidi kwako katika kazi, na ni majukumu gani unapenda kufanya.\n- **Pata mapendekezo:** Mwishowe, nitakupendekezea njia za kukuza taaluma na kazi mahususi zinazolingana na wasifu wako.\n\n**Unaweza kuwasiliana nami kwa Kiswahili, au Kiingereza (English works perfectly too!).** Usijali kuhusu kutumia lugha rasmi sana — tumia tu maneno yako ya kawaida, au hata kuchanganya lugha!\n\nWasifu na CV yako inajihifadhi yenyewe (saves automatically) tunapoendelea, kwa hivyo ukihitaji kupumzika kidogo, unaweza kurudi na kuendelea pale ulipoishia. Pia unaweza kurudi wakati wowote kupakua (download) au kurekebisha CV yako, hata baada ya mazungumzo yetu kuisha!\n\n**Uko tayari kuanza? / Ready to get started?** 🚀\nIli kuanza, andika tu \"Niko tayari\" au \"Twende!\" kisha utume.\n*(To begin, just type \"I am ready\" or \"Let's go!\" and hit send!)*", "collectExperiences": { "didNotUnderstand": "Samahani, sikuelewa. Tafadhali sema tena kwa njia nyingine?", "moveToOtherExperiences": "Tuendelee na uzoefu mwingine wa kazi.",