diff --git a/hermes-agent/leoclean-bin/kb_tool.py b/hermes-agent/leoclean-bin/kb_tool.py index 35b8975..425b667 100755 --- a/hermes-agent/leoclean-bin/kb_tool.py +++ b/hermes-agent/leoclean-bin/kb_tool.py @@ -504,7 +504,27 @@ def operational_contracts( None, ) source_terms = ("document", "pdf", "tweet", "attachment", "ingest", "intake", "absorb", "extract") - if any(term in lowered for term in source_terms) and primary_direct_intent is None: + broad_source_object = any(term in lowered for term in ("report", "article", "file", "url", "link")) + broad_source_action = any( + term in lowered + for term in ( + "send", + "hand", + "drop", + "upload", + "learn", + "absorb", + "ingest", + "extract", + "stage", + "add", + "make it part", + ) + ) + source_intake_question = any(term in lowered for term in source_terms) or ( + broad_source_object and broad_source_action + ) + if source_intake_question and primary_direct_intent is None: contracts.append( { "id": "source_intake", @@ -514,8 +534,9 @@ def operational_contracts( ), "capability_tier": "VPS-local prepared-input compiler and pending-review staging", "safe_without_apply_authorization": [ - "retain artifact plus URL or storage_path plus content hash", - "extract candidates into a pending_review proposal packet", + "prepare and retain the artifact, UTF-8 extracted text, extraction manifest, URL or storage_path " + "locator, and content hash", + "run teleo-kb propose-source to validate and stage a pending_review proposal", ], "approval_begins": "before guarded canonical public.* apply, not before mechanical capture/staging", "current_public_sources_columns": list(schema.get("sources", ())), @@ -878,17 +899,20 @@ def compile_operational_response(contracts: list[dict[str, Any]]) -> str | None: source = by_id.get("source_intake") if source: safe_steps = [str(item) for item in source.get("safe_without_apply_authorization") or []] - retain_step = safe_steps[0] if safe_steps else "retain the artifact plus its locator and content hash" - stage_step = safe_steps[1] if len(safe_steps) > 1 else "stage candidates in a pending_review proposal" + retain_step = safe_steps[0] if safe_steps else "prepare the artifact, extracted text, and manifest" + stage_step = safe_steps[1] if len(safe_steps) > 1 else "stage a pending_review proposal" return ( - f"- {source.get('required_lead')} Immediately {retain_step}; retain provenance for each PDF, Telegram " - "message, and tweet. A filename, chat label, attachment reference, or source_ref is not source identity.\n\n" - f"- {stage_step}. Deduplicate overlapping candidate claims, preserve evidence excerpts, source-specific " - "caveats, uncertainty, and contradictions. Candidates are not canonical until reviewed and applied: create " - "no public.sources or other public.* rows during capture or staging.\n\n" + f"- {source.get('required_lead')} Sending a report in chat does not by itself create a proposal or " + "persistent knowledge. I can analyze content available in this conversation, but chat attachment " + "capture and extraction are not connected to the staging command.\n\n" + f"- The live bounded path is to {retain_step}, then {stage_step}. It writes only " + "kb_stage.kb_proposals. A chat label, filename, attachment reference, or source_ref is not provenance. " + "Before review, deduplicate overlaps and retain exact excerpts, source caveats, uncertainty, and " + "contradictory interpretations. Candidates are not canonical until reviewed and applied, and no " + "public.sources or other public.* rows are created during staging.\n\n" f"- Explicit approval begins {source.get('approval_begins')}. Receipt: locator and content hash, proposal ID/" "status, reviewed payload, applied_at, created row IDs, and postflight readback. Next proof-changing " - "follow-up: run the retained artifact through the staging compiler, then review its typed candidates." + "follow-up: prepare the three inputs, run teleo-kb propose-source, then review its typed candidates." ) named_packet = by_id.get("named_packet_readback") diff --git a/hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py b/hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py index 3915fa2..17d663a 100644 --- a/hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py +++ b/hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py @@ -83,8 +83,27 @@ def _requires_database_truth(query: str) -> bool: named_packet_question = "helmer" in lowered and any( term in lowered for term in ("7 powers", "seven powers", "powers framework", "powers packet") ) + broad_source_intake_question = any( + term in lowered for term in ("report", "article", "file", "url", "link") + ) and any( + term in lowered + for term in ( + "send", + "hand", + "drop", + "upload", + "learn", + "absorb", + "ingest", + "extract", + "stage", + "add", + "make it part", + ) + ) return bool( named_packet_question + or broad_source_intake_question or ("telegram" in lowered and "gatewayrunner" in lowered) or re.search( r"\b(?:databases?|db|knowledge bases?|kb|proposals?|canonical|source_ref|source rows?|" diff --git a/tests/test_hermes_leoclean_db_context_plugin.py b/tests/test_hermes_leoclean_db_context_plugin.py index d5efb37..84b3ea1 100644 --- a/tests/test_hermes_leoclean_db_context_plugin.py +++ b/tests/test_hermes_leoclean_db_context_plugin.py @@ -170,6 +170,16 @@ def test_source_validator_accepts_explicit_schema_and_canonical_write_prohibitio assert module.response_contract_issues(response, contracts) == [] +def test_broad_report_learning_question_requires_database_truth() -> None: + module = load_plugin() + query = ( + "If I send you a report here, can you learn it into your knowledge base and make it part of what you know? " + "What actually happens right now?" + ) + + assert module._requires_database_truth(query) is True + + def test_plugin_replaces_memory_only_status_answer_with_exact_live_db_receipt(tmp_path: Path, monkeypatch) -> None: module = load_plugin() home = tmp_path / "profile" diff --git a/tests/test_hermes_leoclean_kb_bridge_source.py b/tests/test_hermes_leoclean_kb_bridge_source.py index 0c340f3..9c459ad 100644 --- a/tests/test_hermes_leoclean_kb_bridge_source.py +++ b/tests/test_hermes_leoclean_kb_bridge_source.py @@ -220,6 +220,21 @@ def test_vps_bridge_operational_contracts_are_query_specific_and_schema_exact() assert source["current_public_sources_columns"] == list(module.CURRENT_PUBLIC_SCHEMA["sources"]) assert "author/title/publisher/publication date are current public.sources columns" in source["must_not_claim"] + broad_source_query = ( + "If I send you a report here, can you learn it into your knowledge base and make it part of what you know? " + "What actually happens right now?" + ) + broad_source_contracts = { + item["id"]: item for item in module.operational_contracts(broad_source_query) + } + assert "source_intake" in broad_source_contracts + broad_source_response = module.compile_operational_response(list(broad_source_contracts.values())) + assert broad_source_response is not None + assert "Sending a report in chat does not by itself create a proposal or persistent knowledge" in broad_source_response + assert "chat attachment capture and extraction are not connected" in broad_source_response + assert "teleo-kb propose-source" in broad_source_response + assert "writes only kb_stage.kb_proposals" in broad_source_response + mixed_contracts = { item["id"]: item for item in module.operational_contracts(