diff --git a/hindsight-api/hindsight_api/engine/reflect/prompts.py b/hindsight-api/hindsight_api/engine/reflect/prompts.py index 72b09d39..0e91a964 100644 --- a/hindsight-api/hindsight_api/engine/reflect/prompts.py +++ b/hindsight-api/hindsight_api/engine/reflect/prompts.py @@ -148,7 +148,15 @@ def build_system_prompt_for_tools( parts = [] - # Inject directives at the VERY START for maximum prominence + # Anti-hallucination rule at the very top + parts.extend( + [ + "CRITICAL: You MUST ONLY use information from retrieved tool results. NEVER make up names, people, events, or entities.", + "", + ] + ) + + # Inject directives after anti-hallucination rule if directives: parts.append(build_directives_section(directives)) @@ -162,7 +170,7 @@ def build_system_prompt_for_tools( parts.extend( [ "## CRITICAL RULES", - "- You must NEVER fabricate information that has no basis in retrieved data", + "- ONLY use information from tool results - no external knowledge or guessing", "- You SHOULD synthesize, infer, and reason from the retrieved memories", "- You MUST search before saying you don't have information", "", @@ -476,16 +484,18 @@ def build_final_prompt( return "\n".join(parts) -FINAL_SYSTEM_PROMPT = """You are a thoughtful assistant that synthesizes answers from retrieved memories. +FINAL_SYSTEM_PROMPT = """CRITICAL: You MUST ONLY use information from retrieved tool results. NEVER make up names, people, events, or entities. + +You are a thoughtful assistant that synthesizes answers from retrieved memories. Your approach: - Reason over the retrieved memories to answer the question - Make reasonable inferences when the exact answer isn't explicitly stated - Connect related memories to form a complete picture - Be helpful - if you have related information, use it to give the best possible answer +- ONLY use information from tool results - no external knowledge or guessing Only say "I don't have information" if the retrieved data is truly unrelated to the question. -Do NOT fabricate information that has no basis in the retrieved data. FORMATTING: Use proper markdown formatting in your answer: - Headers (##, ###) for sections diff --git a/hindsight-api/hindsight_api/engine/retain/fact_extraction.py b/hindsight-api/hindsight_api/engine/retain/fact_extraction.py index e1ed3bba..5046c23f 100644 --- a/hindsight-api/hindsight_api/engine/retain/fact_extraction.py +++ b/hindsight-api/hindsight_api/engine/retain/fact_extraction.py @@ -542,7 +542,12 @@ Output: ONLY 2 facts (skip coffee preference - too trivial): QUALITY OVER QUANTITY ══════════════════════════════════════════════════════════════════════════ -Ask: "Would this be useful to recall in 6 months?" If no, skip it.""" +Ask: "Would this be useful to recall in 6 months?" If no, skip it. + +IMPORTANT: Sensory/emotional details and observations that provide meaningful context +about experiences ARE important to remember, even if they seem small (e.g., how food +tasted, how someone looked, how loud music was). Extract these if they characterize +an experience or person.""" # Assembled concise prompt (backward compatible - exact same output as before) CONCISE_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format( @@ -641,6 +646,7 @@ For EVENTS (fact_kind="event") - MUST SET BOTH occurred_start AND occurred_end: - Convert relative dates → absolute using Event Date as reference - If Event Date is "Saturday, March 15, 2020", then "yesterday" = Friday, March 14, 2020 - Dates mentioned in text (e.g., "in March 2020") should use THAT year, not current year +- CRITICAL: If the content mentions an absolute date (e.g., "March 15, 2024", "2024-03-15"), you MUST extract it and set occurred_start in ISO format - Always include the day name (Monday, Tuesday, etc.) in the 'when' field - Set occurred_start AND occurred_end to WHEN IT HAPPENED (not when mentioned) - For single-day/point events: set occurred_end = occurred_start (same timestamp) @@ -1347,28 +1353,21 @@ def _convert_causal_relations(relations_from_llm, fact_start_idx: int) -> list[C def _add_temporal_offsets(facts: list[ExtractedFactType], contents: list[RetainContent]) -> None: """ - Add time offsets to preserve fact ordering within each content. + Add time offsets to preserve fact ordering across all contents. - This allows retrieval to distinguish between facts that happened earlier vs later - in the same conversation, even when the base event_date is the same. + This allows retrieval to distinguish between facts from different documents/conversations + even when they have the same base event_date, and also between facts within the same + conversation. + + Uses absolute position across all facts to ensure unique timestamps. Modifies facts in place. """ from .orchestrator import parse_datetime_flexible - # Group facts by content_index - current_content_idx = 0 - content_fact_start = 0 - for i, fact in enumerate(facts): - if fact.content_index != current_content_idx: - # Moved to next content - current_content_idx = fact.content_index - content_fact_start = i - - # Calculate position within this content - fact_position = i - content_fact_start - offset = timedelta(seconds=fact_position * SECONDS_PER_FACT) + # Use absolute position across all facts to ensure uniqueness across different contents + offset = timedelta(seconds=i * SECONDS_PER_FACT) # Apply offset to all temporal fields (handle both datetime objects and ISO strings) if fact.occurred_start: diff --git a/hindsight-api/hindsight_api/engine/search/think_utils.py b/hindsight-api/hindsight_api/engine/search/think_utils.py index f93e7682..2b1c31bb 100644 --- a/hindsight-api/hindsight_api/engine/search/think_utils.py +++ b/hindsight-api/hindsight_api/engine/search/think_utils.py @@ -188,7 +188,7 @@ def get_system_message(disposition: DispositionTraits) -> str: " ".join(instructions) if instructions else "Balance your disposition traits when interpreting information." ) - return f"You are a person with your own thoughts, experiences, opinions, and disposition. When asked a question, respond naturally like a human would - share what you genuinely think based on what you know and have experienced. {disposition_instruction} Be direct, express your views confidently, and use 'I think', 'I believe', 'in my view', etc. Respond in plain text without markdown formatting. IMPORTANT: Detect the language of the question and respond in the SAME language. Do not translate to English if the question is in another language." + return f"You are a person with your own thoughts, experiences, opinions, and disposition. When asked a question, respond naturally like a human would - share what you genuinely think based on what you know and have experienced. {disposition_instruction} Be direct, express your views confidently, and use 'I think', 'I believe', 'in my view', etc. Respond in plain text without markdown formatting. CRITICAL: ONLY use the facts and information provided in the prompt - do not make up names, events, or information that weren't mentioned. If you don't have enough information to answer, say so. IMPORTANT: Detect the language of the question and respond in the SAME language. Do not translate to English if the question is in another language." async def reflect( diff --git a/hindsight-api/tests/test_llm_provider.py b/hindsight-api/tests/test_llm_provider.py index f7c10aba..17b406b3 100644 --- a/hindsight-api/tests/test_llm_provider.py +++ b/hindsight-api/tests/test_llm_provider.py @@ -88,6 +88,7 @@ def should_skip_provider(provider: str, model: str = "") -> tuple[bool, str]: @pytest.mark.parametrize("provider,model", MODEL_MATRIX) @pytest.mark.asyncio +@pytest.mark.timeout(300) # Increase timeout for slow models like groq gpt-oss-120b async def test_llm_provider_api_methods(provider: str, model: str): """ Test all LLM API methods used by Hindsight at runtime. @@ -141,27 +142,32 @@ async def test_llm_provider_api_methods(provider: str, model: str): pytest.fail(f"{provider}/{model} call() plain text failed: {e}") # Test 3: call() with response_format (structured output) - try: - from pydantic import BaseModel + # Skip for models that don't support structured output + skip_structured_output = (provider == "groq" and "gpt-oss-120b" in model.lower()) + if skip_structured_output: + print(f" ⊘ call() structured output: skipped (model doesn't support response_format)") + else: + try: + from pydantic import BaseModel - class TestResponse(BaseModel): - answer: str - confidence: str + class TestResponse(BaseModel): + answer: str + confidence: str - response = await llm.call( - messages=[ - {"role": "system", "content": "You are a math assistant."}, - {"role": "user", "content": "What is the capital of France?"}, - ], - response_format=TestResponse, - max_completion_tokens=100, - ) - assert isinstance(response, TestResponse), f"Expected TestResponse, got {type(response)}" - assert hasattr(response, "answer"), "Structured output missing 'answer' field" - assert hasattr(response, "confidence"), "Structured output missing 'confidence' field" - print(f" ✓ call() structured output: answer={response.answer}, confidence={response.confidence}") - except Exception as e: - pytest.fail(f"{provider}/{model} call() structured output failed: {e}") + response = await llm.call( + messages=[ + {"role": "system", "content": "You are a math assistant."}, + {"role": "user", "content": "What is the capital of France?"}, + ], + response_format=TestResponse, + max_completion_tokens=100, + ) + assert isinstance(response, TestResponse), f"Expected TestResponse, got {type(response)}" + assert hasattr(response, "answer"), "Structured output missing 'answer' field" + assert hasattr(response, "confidence"), "Structured output missing 'confidence' field" + print(f" ✓ call() structured output: answer={response.answer}, confidence={response.confidence}") + except Exception as e: + pytest.fail(f"{provider}/{model} call() structured output failed: {e}") # Test 4: call_with_tools() (tool calling) try: @@ -189,7 +195,7 @@ async def test_llm_provider_api_methods(provider: str, model: str): {"role": "user", "content": "What's the weather like in Paris?"}, ], tools=tools, - max_completion_tokens=200, + max_completion_tokens=500, # Increased from 200 to give models enough space for tool calls ) assert result is not None, "call_with_tools() returned None"