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
29 changes: 29 additions & 0 deletions backend/app/agent/collect_experiences_agent/_conversation_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@ def _find_incomplete_experiences(collected_data: list[CollectedData]) -> list[tu
return incomplete_experiences


def fill_incomplete_fields_as_declined(
collected_data: list[CollectedData],
work_type: WorkType | None,
) -> None:
"""
Set None to "" for completeness fields (start_date, end_date, company, location)
on experiences of the given work type. Treats missing data as user declined to provide.
Mutates the experiences in place.
"""
if work_type is None:
return
logger = logging.getLogger(__name__)
key = work_type.name
for exp in collected_data:
if exp.work_type and exp.work_type.strip() == key:
if exp.start_date is None:
logger.warning("Filling incomplete start_date as declined for %s experience: %s", key, exp.experience_title)
exp.start_date = ""
if exp.end_date is None:
logger.warning("Filling incomplete end_date as declined for %s experience: %s", key, exp.experience_title)
exp.end_date = ""
if exp.company is None:
logger.warning("Filling incomplete company as declined for %s experience: %s", key, exp.experience_title)
exp.company = ""
if exp.location is None:
logger.warning("Filling incomplete location as declined for %s experience: %s", key, exp.experience_title)
exp.location = ""


def _get_incomplete_experiences_instructions(collected_data: list[CollectedData]) -> str:
"""
Generate instructions for handling incomplete experiences.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
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
from app.agent.experience.work_type import WorkType
from common_libs.llm.generative_models import GeminiGenerativeLLM
from common_libs.llm.models_utils import LLMConfig, ZERO_TEMPERATURE_GENERATION_CONFIG, JSON_GENERATION_CONFIG, \
get_config_variation
from common_libs.llm.schema_builder import with_response_schema
from common_libs.retry import Retry
from ._conversation_llm import _find_incomplete_experiences, _get_experience_type, _ask_experience_type_question
from ._conversation_llm import _get_experience_type, _ask_experience_type_question
from ._types import CollectedData
from ..experience import WorkType

_TAGS_TO_FILTER = [
"system instructions",
Expand Down Expand Up @@ -99,18 +99,15 @@ async def execute(self,
explored_types: list[WorkType],
conversation_context: ConversationContext,
user_input: AgentInput) -> tuple[TransitionDecision, Optional[TransitionReasoning], list[LLMStats]]:

# Rule-based check 1: Incomplete experiences
incomplete_experiences = _find_incomplete_experiences(collected_data)
if incomplete_experiences:
incomplete_required = _find_incomplete_required_for_work_type(collected_data, exploring_type)
if incomplete_required:
self._logger.info(
"Incomplete experiences found - returning CONTINUE. "
"Incomplete required fields (title/work_type) - returning CONTINUE. "
"Incomplete experiences: %s",
[(idx, exp.experience_title, missing) for idx, exp, missing in incomplete_experiences]
[(idx, exp.experience_title, missing) for idx, exp, missing in incomplete_required]
)
return TransitionDecision.CONTINUE, None, []



cleaned_experience_dicts = []
for collected_item in collected_data:
collected_item_dict = collected_item.model_dump(exclude={"defined_at_turn_number"})
Expand Down Expand Up @@ -288,7 +285,7 @@ async def _internal_execute(self,

#Constraints
- Use semantic understanding, not keyword matching
- If incomplete experiences exist, continue_current_type must be true
- When the user clearly indicates no more experiences of this type (e.g. "no", "nope"), return continue_current_type=false
- If unexplored_types is not empty, done_with_collection must be false

#Collected Experience Data
Expand Down Expand Up @@ -333,3 +330,28 @@ async def _internal_execute(self,

You must complete the entire JSON object including the closing brace }}.
"""


def _find_incomplete_required_for_work_type(
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).
Used for early-exit: if any have missing required fields, we must CONTINUE to gather them.
Optional fields (start_date, end_date, company, location) are NOT checked here.
"""
if exploring_type is None:
return []
key = exploring_type.name
result = []
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()):
missing.append("experience_title")
if not (exp.work_type and exp.work_type.strip()):
missing.append("work_type")
if missing:
result.append((i, exp, missing))
return result
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from app.agent.agent_types import AgentInput, AgentOutput
from app.agent.agent_types import AgentType
from app.agent.collect_experiences_agent._conversation_llm import _ConversationLLM, ConversationLLMAgentOutput, \
_get_experience_type
_get_experience_type, fill_incomplete_fields_as_declined
from app.agent.collect_experiences_agent._dataextraction_llm import _DataExtractionLLM
from app.agent.collect_experiences_agent._transition_decision_tool import TransitionDecisionTool, TransitionDecision
from app.agent.collect_experiences_agent._types import CollectedData
Expand Down Expand Up @@ -184,21 +184,35 @@ async def execute(self, user_input: AgentInput,

reasoning_text = transition_reasoning.reasoning if transition_reasoning else "No reasoning provided"

if transition_decision == TransitionDecision.END_WORKTYPE and self._state.unexplored_types:
explored_type = self._state.unexplored_types.pop(0)
self._state.explored_types.append(explored_type)
self.logger.info(
"Transition decision: END_WORKTYPE - Explored work type: %s"
"\n - remaining types: %s"
"\n - discovered experiences so far: %s"
"\n - reasoning: %s",
explored_type,
self._state.unexplored_types,
self._state.collected_data,
reasoning_text
)

next_exploring_type = self._state.unexplored_types[0] if len(self._state.unexplored_types) > 0 else None
if transition_decision == TransitionDecision.END_WORKTYPE:
did_update = False
# if decision is to end the exploration of the current work type, we update null fields to ""
if exploring_type is not None and exploring_type in self._state.unexplored_types:
fill_incomplete_fields_as_declined(
self._state.collected_data, exploring_type
)
self._state.unexplored_types.remove(exploring_type)
self._state.explored_types.append(exploring_type)
did_update = True
self.logger.info(
"Transition decision: END_WORKTYPE - Explored work type: %s"
"\n - remaining types: %s"
"\n - discovered experiences so far: %s"
"\n - reasoning: %s",
exploring_type,
self._state.unexplored_types,
self._state.collected_data,
reasoning_text
)
# exit if no unexplored types left
if not did_update and not self._state.unexplored_types:
conversation_llm_output.finished = True
self.logger.info(
"Transition decision: END_WORKTYPE with no unexplored types - treating as END_CONVERSATION"
)
return conversation_llm_output

next_exploring_type = self._state.unexplored_types[0] if self._state.unexplored_types else None
transition_message: str
if next_exploring_type is not None:
transition_message = (
Expand Down