Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ EMBEDDINGS_MODEL_NAME=text-embedding-005
BACKEND_SENTRY_DSN=https://
BACKEND_ENABLE_SENTRY=False
# For supported options see: app.sentry_init.BackendSentryConfig
BACKEND_SENTRY_CONFIG={"tracesSampleRate": 0.2, "enableLogs": false, "logLevel": "info", "eventLevel": "error"}
BACKEND_SENTRY_CONFIG='{"tracesSampleRate": 0.2, "enableLogs": false, "logLevel": "info", "eventLevel": "error"}'

# Country settings
DEFAULT_COUNTRY_OF_USER=Unspecified
Expand All @@ -53,7 +53,7 @@ GLOBAL_DISABLE_REGISTRATION_CODE=False
BACKEND_FEATURES='{}'

# Configuration for the experience pipeline
BACKEND_EXPERIENCE_PIPELINE_CONFIG={"number_of_clusters": 5, "number_of_top_skills_to_pick_per_cluster": 1}
BACKEND_EXPERIENCE_PIPELINE_CONFIG='{"number_of_clusters": 5, "number_of_top_skills_to_pick_per_cluster": 1}'

# CV storage and limits (optional; required to persist uploads)
GLOBAL_ENABLE_CV_UPLOAD=<True/False>
Expand All @@ -62,8 +62,17 @@ BACKEND_CV_MAX_UPLOADS_PER_USER=<INTEGER>
BACKEND_CV_RATE_LIMIT_PER_MINUTE=<INTEGER>


# Locales
BACKEND_LANGUAGE_CONFIG='{"default_locale":"en-US","available_locales":[{"locale":"en-US","date_format":"MM/DD/YYYY"}]}'
# Language Configurations
BACKEND_LANGUAGE_CONFIG='{
"conversation_fallback_locale": "en-US",
"reporting_locale": "en-US",
"available_locales": [
{
"locale": "en-US",
"date_format": "MM/DD/YYYY"
}
]
}'

# Branding settings
GLOBAL_PRODUCT_NAME=Compass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from app.agent.config import AgentsConfig
from app.agent.llm_caller import LLMCaller
from app.agent.penalty import get_penalty
from app.agent.prompt_template import get_language_style
from app.agent.prompt_template import sanitize_input
from app.conversation_memory.conversation_formatter import ConversationHistoryFormatter
from app.conversation_memory.conversation_memory_types import ConversationContext
Expand Down Expand Up @@ -81,14 +80,13 @@ def _get_llm(collected_data_json: str, temperature_config: Optional[dict] = None
return GeminiGenerativeLLM(
system_instructions=_SYSTEM_INSTRUCTIONS.format(
collected_data=collected_data_json,
language_style=get_language_style()
),
config=LLMConfig(
language_model_name=AgentsConfig.deep_reasoning_model,
generation_config=ZERO_TEMPERATURE_GENERATION_CONFIG
| JSON_GENERATION_CONFIG
| temperature_config
| with_response_schema(_TransitionDecisionOutput)
| JSON_GENERATION_CONFIG
| temperature_config
| with_response_schema(_TransitionDecisionOutput)
))

async def execute(self,
Expand All @@ -98,7 +96,8 @@ async def execute(self,
unexplored_types: list[WorkType],
explored_types: list[WorkType],
conversation_context: ConversationContext,
user_input: AgentInput) -> tuple[TransitionDecision, Optional[TransitionReasoning], list[LLMStats]]:
user_input: AgentInput) -> tuple[
TransitionDecision, Optional[TransitionReasoning], list[LLMStats]]:
incomplete_required = _find_incomplete_required_for_work_type(collected_data, exploring_type)
if incomplete_required:
self._logger.info(
Expand All @@ -112,20 +111,20 @@ async def execute(self,
for collected_item in collected_data:
collected_item_dict = collected_item.model_dump(exclude={"defined_at_turn_number"})
cleaned_experience_dicts.append(collected_item_dict)

json_data = json.dumps(cleaned_experience_dicts, indent=2)

conversation_history = ConversationHistoryFormatter.format_history_for_agent_generative_prompt(
conversation_context
)

exploring_type_str = exploring_type.name if exploring_type else "None"
unexplored_types_str = ", ".join([wt.name for wt in unexplored_types])
explored_types_str = ", ".join([wt.name for wt in explored_types])

exploring_type_description = _get_experience_type(exploring_type) if exploring_type else "None"
work_type_mapping = _generate_work_type_mapping()

prompt = _PROMPT_TEMPLATE.format(
user_input=user_input.message,
conversation_history=conversation_history,
Expand All @@ -136,42 +135,42 @@ async def execute(self,
unexplored_types=unexplored_types_str,
explored_types=explored_types_str
)

_llm_stats = []
_reasoning = None

async def _callback(attempt: int, max_retries: int) -> tuple[TransitionDecision, float, BaseException | None]:
temperature_config = get_config_variation(start_temperature=0.0, end_temperature=0.1,
start_top_p=0.95, end_top_p=1.0,
attempt=attempt, max_retries=max_retries)

llm = self._get_llm(collected_data_json=json_data, temperature_config=temperature_config)
self._logger.debug("Calling transition decision LLM with temperature: %s, top_p: %s",
temperature_config["temperature"],
temperature_config["top_p"])

data, reasoning, llm_stats, penalty, error = await self._internal_execute(
llm=llm,
llm=llm,
prompt=prompt,
unexplored_types=unexplored_types
)

nonlocal _reasoning
_reasoning = reasoning
_llm_stats.extend(llm_stats)

return data, penalty, error

result, _result_penalty, _error = await Retry[TransitionDecision].call_with_penalty(
callback=_callback, logger=self._logger)

reasoning = _reasoning
if reasoning is None:
reasoning = TransitionReasoning(
reasoning="No reasoning provided - error occurred during LLM call",
confidence="low"
)

# Additional validation: ensure END_CONVERSATION only when all types explored
if result == TransitionDecision.END_CONVERSATION and unexplored_types:
self._logger.warning(
Expand All @@ -185,7 +184,7 @@ async def _callback(attempt: int, max_retries: int) -> tuple[TransitionDecision,
reasoning=f"Invalid END_CONVERSATION decision - unexplored_types not empty: {[wt.name for wt in unexplored_types]}. Original reasoning: {reasoning.reasoning if reasoning else 'None'}",
confidence="high"
)

self._logger.info(
"Transition decision: %s. "
"Exploring type: %s, Unexplored types: %s, Explored types: %s, "
Expand All @@ -204,15 +203,16 @@ async def _internal_execute(self,
*,
llm: GeminiGenerativeLLM,
prompt: str,
unexplored_types: list[WorkType]) -> tuple[TransitionDecision, TransitionReasoning, list[LLMStats], float, BaseException | None]:

unexplored_types: list[WorkType]) -> tuple[
TransitionDecision, TransitionReasoning, list[LLMStats], float, BaseException | None]:

no_response_penalty_level = 3
response_data, llm_stats = await self._llm_caller.call_llm(
llm=llm,
llm_input=sanitize_input(prompt, _TAGS_TO_FILTER),
logger=self._logger
)

if not response_data:
_error = ValueError("LLM did not return any output")
self._logger.error(_error, stack_info=True)
Expand All @@ -225,16 +225,17 @@ async def _internal_execute(self,
continue_current_type = response_data.continue_current_type
done_with_collection = response_data.done_with_collection
reasoning_text = response_data.reasoning if hasattr(response_data, 'reasoning') else "No reasoning provided"

# Truncate reasoning if too long to prevent bloat
if len(reasoning_text) > MAX_REASONING_LENGTH:
reasoning_text = reasoning_text[:MAX_REASONING_LENGTH].rsplit('.', 1)[0] + "."
self._logger.warning("Reasoning truncated from %d to %d characters", len(response_data.reasoning), len(reasoning_text))

self._logger.warning("Reasoning truncated from %d to %d characters", len(response_data.reasoning),
len(reasoning_text))

# Ensure reasoning doesn't exceed limit (safety check)
if len(reasoning_text) > MAX_REASONING_LENGTH:
reasoning_text = reasoning_text[:MAX_REASONING_LENGTH]

# Validate done_with_collection against state (deterministic check)
if done_with_collection and unexplored_types:
self._logger.warning(
Expand All @@ -244,23 +245,23 @@ async def _internal_execute(self,
reasoning_text
)
done_with_collection = False

# Map binary outputs to transition decision
if continue_current_type:
decision = TransitionDecision.CONTINUE
elif done_with_collection:
decision = TransitionDecision.END_CONVERSATION
else:
decision = TransitionDecision.END_WORKTYPE
self._logger.debug("Transition decision: %s (continue_current_type=%s, done_with_collection=%s). Reasoning: %s",
decision, continue_current_type, done_with_collection, reasoning_text)

self._logger.debug("Transition decision: %s (continue_current_type=%s, done_with_collection=%s). Reasoning: %s",
decision, continue_current_type, done_with_collection, reasoning_text)

reasoning = TransitionReasoning(
reasoning=reasoning_text,
confidence="medium"
)

return decision, reasoning, llm_stats, 0, None


Expand All @@ -269,8 +270,6 @@ async def _internal_execute(self,
#Role
You decide when to transition between phases in a work experience collection conversation.

{language_style}

#Decision Logic
Answer two boolean questions:

Expand Down Expand Up @@ -333,8 +332,8 @@ async def _internal_execute(self,


def _find_incomplete_required_for_work_type(
collected_data: list[CollectedData],
exploring_type: WorkType | None,
collected_data: list[CollectedData],
exploring_type: WorkType | None,
) -> list[tuple[int, CollectedData, list[str]]]:
"""
Find experiences of the given work type that lack required fields (experience_title, work_type).
Expand All @@ -345,7 +344,7 @@ def _find_incomplete_required_for_work_type(
return []
key = exploring_type.name
result = []
for i, exp in enumerate (collected_data):
for i, exp in enumerate(collected_data):
if exp.work_type and exp.work_type.strip() == key:
missing = []
if not (exp.experience_title and exp.experience_title.strip()):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ def _get_llm(temperature_config: Optional[dict] = None) -> GeminiGenerativeLLM:
if temperature_config is None:
temperature_config = {}

language_style = get_language_style(prompt_intent="application_state")
return GeminiGenerativeLLM(
system_instructions=_SYSTEM_INSTRUCTIONS.format(language_style=get_language_style()),
system_instructions=_SYSTEM_INSTRUCTIONS.format(language_style=language_style),
config=LLMConfig(
generation_config=ZERO_TEMPERATURE_GENERATION_CONFIG | JSON_GENERATION_CONFIG | {
"max_output_tokens": 3000
Expand Down Expand Up @@ -113,7 +114,8 @@ async def _callback(attempt: int, max_retries: int) -> tuple[ExtractedData, floa

return data, penality, error

result, _result_penalty, _error = await Retry[ExtractedData].call_with_penalty(callback=_callback, logger=self._logger)
result, _result_penalty, _error = await Retry[ExtractedData].call_with_penalty(callback=_callback,
logger=self._logger)

return result, _llm_stats

Expand Down Expand Up @@ -176,8 +178,8 @@ async def _internal_execute(self,
Extract the title of the experience from the '<User's Last Input>'.
For unpaid work, use the kind of work done (e.g. "Helping Neighbors", "Volunteering" etc).
Make sure that the user is actually referring to an experience they have have.
When summarizing a user-stated action (e.g., "I sell tomatoes"), convert it directly into a gerund-phrase experience title (e.g., "Selling Tomatoes").
Return a string value containing the title of the experience.
When summarizing a user-stated action (e.g., "I sell tomatoes"), convert it directly into a gerund-phrase experience title (e.g., "Selling Tomatoes" in respective language).
Return a string value containing the title of the experience in respective language.
Use `null`: If the user has not mentioned their `experience title` and has not yet been asked to provide it.
Use "": If the user explicitly declines to provide their `experience title` when explicitly asked, or requests that previously stored `experience title` data be deleted.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _get_llm(previously_extracted_data: str, temperature_config: Optional[dict]

return GeminiGenerativeLLM(
system_instructions=_SYSTEM_INSTRUCTIONS.format(previously_extracted_data=previously_extracted_data,
language_style=get_language_style()),
language_style=get_language_style(prompt_intent="application_state")),
config=LLMConfig(
generation_config=ZERO_TEMPERATURE_GENERATION_CONFIG | JSON_GENERATION_CONFIG | {
"max_output_tokens": 3000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _get_system_instructions(self) -> str:
return _SYSTEM_INSTRUCTIONS.format(
work_type_definitions=WORK_TYPE_DEFINITIONS_FOR_PROMPT,
current_date=current_date_formatted,
language_style=get_language_style(),
language_style=get_language_style(prompt_intent="application_state"),
date_format_full=date_formats.full,
date_format_month_year=date_formats.month_year,
date_format_year=date_formats.year_only,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/agent/experience/_experience_summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def get_system_instructions(*, country_of_user: Country) -> str:
""")
return replace_placeholders_with_indent(
_summarize_system_instructions,
language_style=get_language_style(),
language_style=get_language_style(prompt_intent="application_state"),
country_instructions=_country_instructions
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _get_system_instructions(country_of_interest: Country, number_of_titles: int
""")
return replace_placeholders_with_indent(system_instructions_template,
country_of_interest=country_of_interest.value,
language_style=get_language_style(),
language_style=get_language_style(prompt_intent="application_state"),
work_type_names=", ".join([work_type.name for work_type in WorkType]),
glossary=glossary_str,
number_of_titles=f"{number_of_titles}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ def _get_system_instructions(entity_type_singular: Literal['skill', 'occupation'

return replace_placeholders_with_indent(system_prompt_template,
entity_type_singular=entity_type_singular,
language_style=get_language_style(),
language_style=get_language_style(prompt_intent="application_state"),
entity_types_plural=entity_types_plural,
entity_types_plural_capitalized=entity_types_plural_capitalized)

Expand Down
Loading
Loading