#!/usr/bin/env python3 """Run a manual, real Zep Cloud validation against a retained test graph. This script is intentionally excluded from the automated test suite. It requires ``ZEP_API_KEY`` at runtime, never prints the key, and deletes the graph unless ``--keep-graph`` is explicitly supplied. If the activity updater cannot be confirmed drained after a failure, the graph is retained to avoid a write/delete race. """ from __future__ import annotations import argparse import json import os import sys import time from dataclasses import dataclass from datetime import datetime, timezone from typing import Any, Iterable # Capture the caller-supplied key before importing MiroFish modules. app.config # deliberately loads the repository .env with override=True, which must not # silently replace the account selected for this explicit validation process. _PROCESS_ZEP_API_KEY = os.environ.get("ZEP_API_KEY", "").strip() from zep_cloud import BatchAddItem from zep_cloud.types import SearchFilters from app.services.graph_builder import BatchSubmission, GraphBuilderService from app.services.zep_entity_reader import ZepEntityReader from app.services.zep_graph_memory_updater import AgentActivity, ZepGraphMemoryUpdater from app.utils.zep_paging import fetch_all_edges, fetch_all_nodes # Keep later code that consults os.environ consistent with the captured value. # All Cloud clients in this script also receive the key explicitly. if _PROCESS_ZEP_API_KEY: os.environ["ZEP_API_KEY"] = _PROCESS_ZEP_API_KEY @dataclass(frozen=True) class SourceEpisode: created_at: str data: str phase: str data_type: str = "text" BASELINE_EPISODES = [ SourceEpisode( "2026-01-05T09:00:00Z", "澜舟科技(企业稳定标识 LZ-TECH)是一家风电智能运维公司。" "公司总部位于星港市,周岚自 2024 年起担任首席执行官(CEO)。", "baseline", ), SourceEpisode( "2026-01-12T10:00:00Z", "澜舟科技研发了产品智巡平台(产品稳定标识 ZHIXUN-01)。" "智巡平台当前处于试点阶段,用于识别风机叶片异常。", "baseline", ), SourceEpisode( "2026-01-20T14:00:00Z", "澜舟科技与海岳能源(企业稳定标识 HY-ENERGY)签署 HY-2026-01 合作协议。" "双方将在东湾风场联合部署智巡平台,协议有效期至 2026 年 12 月 31 日。", "baseline", ), SourceEpisode( "2026-02-02T09:30:00Z", "陈屿担任澜舟科技智巡平台的项目负责人,负责东湾风场部署。" "公司经营仍由首席执行官周岚负责。", "baseline", ), SourceEpisode( "2026-02-20T18:00:00Z", "东湾风场的智巡平台试点发现了 12 处叶片异常," "使海岳能源的非计划停机时间降低了 18%。", "baseline", ), SourceEpisode( "2026-03-05T11:00:00Z", "海岳能源确认澜舟科技是智巡平台的开发方,陈屿是实施项目负责人。" "海岳能源计划在试点验收后成为首批商业客户。", "baseline", ), SourceEpisode( "2026-03-15T16:00:00Z", "澜舟科技董事会批准智巡平台在 2026 年 4 月 1 日从试点阶段转为商业发布。" "周岚以首席执行官身份签署了发布决议。", "baseline", ), SourceEpisode( "2026-04-01T08:00:00Z", "智巡平台今天正式商业发布,不再处于试点阶段。" "海岳能源成为智巡平台首个商业客户,陈屿继续负责交付。", "baseline", ), SourceEpisode( "2026-04-18T13:00:00Z", "澜舟科技在星港市总部公布运营数据:智巡平台已覆盖 60 台风机," "海岳能源仍是 HY-2026-01 协议下的合作伙伴和客户。", "baseline", ), SourceEpisode( "2026-04-30T17:00:00Z", "截至 2026 年 4 月 30 日,周岚仍担任澜舟科技首席执行官," "澜舟科技总部仍在星港市;陈屿担任智巡平台项目负责人。", "baseline", ), ] TEMPORAL_UPDATES = [ SourceEpisode( "2026-05-10T09:00:00Z", "澜舟科技董事会宣布,自 2026 年 5 月 10 日起,周岚不再担任首席执行官。" "陈屿正式接任澜舟科技首席执行官,周岚转任首席战略顾问。", "leadership_change", ), SourceEpisode( "2026-06-01T09:00:00Z", "自 2026 年 6 月 1 日起,澜舟科技总部已从星港市迁至海城市。" "星港市原总部不再是公司总部,现改为澜舟科技研发中心。", "headquarters_change", ), SourceEpisode( "2026-06-15T12:00:00Z", "澜舟科技与海岳能源已于 2026 年 6 月 15 日提前终止 HY-2026-01 联合部署协议。" "海岳能源不再是澜舟科技的联合部署合作伙伴,但仍是智巡平台客户。", "partnership_change", ), SourceEpisode( "2026-06-20T18:00:00Z", json.dumps( { "event": "product_metrics_update", "company_id": "LZ-TECH", "product_id": "ZHIXUN-01", "product_name": "智巡平台", "commercial_status": "commercial", "covered_turbines": 120, "active_customer": "海岳能源", "as_of": "2026-06-20", }, ensure_ascii=False, ), "json_update", "json", ), ] ONTOLOGY = { "entity_types": [ { "name": "Person", "description": "A named person involved in company governance or delivery.", "attributes": [{"name": "current_role", "description": "The person's current role."}], }, { "name": "Company", "description": "A company or commercial organization.", "attributes": [{"name": "stable_id", "description": "A stable company identifier."}], }, { "name": "Product", "description": "A named software or industrial product.", "attributes": [{"name": "lifecycle_stage", "description": "The product lifecycle stage."}], }, { "name": "Place", "description": "A city, office, wind farm, or other named location.", "attributes": [{"name": "place_kind", "description": "The kind of place."}], }, { "name": "Agreement", "description": "A named commercial agreement or contract.", "attributes": [{"name": "agreement_status", "description": "The agreement status."}], }, ], "edge_types": [ { "name": "HOLDS_ROLE_AT", "description": "A person holds a named role at a company.", "attributes": [{"name": "title", "description": "The person's title."}], "source_targets": [{"source": "Person", "target": "Company"}], }, { "name": "DEVELOPS", "description": "A company develops a product.", "attributes": [{"name": "product_status", "description": "The product status."}], "source_targets": [{"source": "Company", "target": "Product"}], }, { "name": "HEADQUARTERED_IN", "description": "A company has its current headquarters in a place.", "attributes": [{"name": "site_status", "description": "The site's headquarters status."}], "source_targets": [{"source": "Company", "target": "Place"}], }, { "name": "PARTNERS_WITH", "description": "A company has a commercial partnership with another company.", "attributes": [{"name": "agreement_id", "description": "The governing agreement identifier."}], "source_targets": [{"source": "Company", "target": "Company"}], }, { "name": "CUSTOMER_OF", "description": "A company is a customer of a product's developer.", "attributes": [{"name": "customer_status", "description": "The customer status."}], "source_targets": [{"source": "Company", "target": "Company"}], }, ], } def _uuid(value: Any) -> str: return str(getattr(value, "uuid_", None) or getattr(value, "uuid", "")) def _status_code(error: Exception) -> int | None: direct = getattr(error, "status_code", None) response = getattr(error, "response", None) return direct or getattr(response, "status_code", None) def _require_process_api_key() -> str: if not _PROCESS_ZEP_API_KEY: raise RuntimeError( "ZEP_API_KEY must be supplied through the process environment" ) return _PROCESS_ZEP_API_KEY def _drain_updater_after_failure( updater: Any, *, started: bool, stop_attempted: bool, ) -> tuple[bool, Exception | None]: """Make one safe drain attempt when the main flow did not call stop().""" if not started: return True, None if stop_attempted: return False, None try: updater.stop() except Exception as error: return False, error return True, None def _cleanup_graph( client: Any, graph_id: str, *, created: bool, keep_graph: bool, updater_started: bool, updater_drained: bool, ) -> dict[str, Any]: """Delete only when no updater can still write to or ingest into the graph.""" if not created: return { "graph_deleted": False, "graph_retained": False, "reason": "graph_not_created", } if keep_graph: return { "graph_deleted": False, "graph_retained": True, "reason": "user_requested", } if updater_started and not updater_drained: return { "graph_deleted": False, "graph_retained": True, "reason": "updater_not_confirmed_drained", } client.graph.delete(graph_id) return { "graph_deleted": True, "graph_retained": False, "reason": "validation_cleanup", } def _wait_for_episode(client: Any, episode_uuid: str, timeout: int) -> Any: deadline = time.monotonic() + timeout while time.monotonic() < deadline: episode = client.graph.episode.get(uuid_=episode_uuid) if getattr(episode, "processed", False): return episode time.sleep(3) raise TimeoutError(f"episode {episode_uuid} did not finish within {timeout}s") def _list_batch_items(client: Any, batch_id: str, page_size: int = 3) -> tuple[list[Any], int]: items: list[Any] = [] cursor: int | None = None pages = 0 while True: response = client.batch.list_items(batch_id=batch_id, limit=page_size, cursor=cursor) pages += 1 items.extend(response.items or []) next_cursor = response.next_cursor if next_cursor is None: return items, pages if next_cursor == cursor: raise RuntimeError("batch item cursor did not advance") cursor = next_cursor def _raw_pages(api_call: Any, graph_id: str, page_size: int = 2) -> tuple[list[Any], int]: items: list[Any] = [] cursor: str | None = None pages = 0 seen: set[str] = set() while True: kwargs: dict[str, Any] = {"limit": page_size} if cursor: kwargs["cursor"] = cursor response = api_call(graph_id, **kwargs) pages += 1 items.extend(list(response.data or [])) next_cursor = response.headers.get("zep-next-cursor") if not next_cursor: return items, pages if next_cursor == cursor or next_cursor in seen: raise RuntimeError("artifact cursor did not advance") seen.add(next_cursor) cursor = next_cursor def _edge_view(edge: Any, node_names: dict[str, str]) -> dict[str, Any]: return { "uuid": _uuid(edge), "name": edge.name, "fact": edge.fact, "source": node_names.get(edge.source_node_uuid, edge.source_node_uuid), "target": node_names.get(edge.target_node_uuid, edge.target_node_uuid), "created_at": edge.created_at, "valid_at": edge.valid_at, "invalid_at": edge.invalid_at, "expired_at": edge.expired_at, "attributes": edge.attributes or {}, } def _node_view(node: Any) -> dict[str, Any]: return { "uuid": _uuid(node), "name": node.name, "labels": node.labels or [], "summary": node.summary, "attributes": node.attributes or {}, } def _search_view(results: Any, node_names: dict[str, str]) -> dict[str, Any]: return { "context": (results.context or "")[:3000], "edges": [_edge_view(edge, node_names) for edge in (results.edges or [])], "nodes": [_node_view(node) for node in (results.nodes or [])], "episode_count": len(results.episodes or []), "observation_count": len(results.observations or []), "thread_summary_count": len(results.thread_summaries or []), } def _episode_to_batch_item(graph_id: str, item: SourceEpisode, index: int) -> BatchAddItem: return BatchAddItem( type="graph_episode", graph_id=graph_id, data=item.data, data_type=item.data_type, created_at=item.created_at, source_description="MiroFish deep Zep Cloud validation corpus", metadata={ "source": "mirofish_zep_deep_validation", "phase": item.phase, "sequence": index, }, ) def _add_and_wait(client: Any, graph_id: str, item: SourceEpisode, timeout: int) -> str: episode = client.graph.add( graph_id=graph_id, type=item.data_type, data=item.data, created_at=item.created_at, source_description="MiroFish temporal Zep Cloud validation update", metadata={"source": "mirofish_zep_deep_validation", "phase": item.phase}, ) episode_uuid = _uuid(episode) if not episode_uuid: raise RuntimeError("graph.add returned no episode UUID") _wait_for_episode(client, episode_uuid, timeout) return episode_uuid def _activities() -> Iterable[AgentActivity]: return [ AgentActivity( platform="twitter", agent_id=101, agent_name="陈屿", action_type="CREATE_POST", action_args={"content": "海城市新总部今天启用,智巡平台商业服务正常运行。"}, round_num=1, timestamp="2026-07-01T09:00:00Z", ), AgentActivity( platform="twitter", agent_id=102, agent_name="周岚", action_type="QUOTE_POST", action_args={ "original_author_name": "陈屿", "original_content": "海城市新总部今天启用,智巡平台商业服务正常运行。", "quote_content": "作为首席战略顾问,我支持陈屿和新的管理团队。", }, round_num=1, timestamp="2026-07-01T09:05:00Z", ), AgentActivity( platform="twitter", agent_id=201, agent_name="海岳能源", action_type="LIKE_POST", action_args={ "post_author_name": "陈屿", "post_content": "海城市新总部今天启用,智巡平台商业服务正常运行。", }, round_num=1, timestamp="2026-07-01T09:06:00Z", ), AgentActivity( platform="twitter", agent_id=201, agent_name="海岳能源", action_type="CREATE_COMMENT", action_args={ "post_author_name": "陈屿", "post_content": "海城市新总部今天启用,智巡平台商业服务正常运行。", "content": "联合部署协议虽已终止,但我们仍是智巡平台客户。", }, round_num=2, timestamp="2026-07-01T09:10:00Z", ), AgentActivity( platform="twitter", agent_id=101, agent_name="陈屿", action_type="FOLLOW", action_args={"target_user_name": "海岳能源"}, round_num=2, timestamp="2026-07-01T09:12:00Z", ), ] def run(args: argparse.Namespace) -> dict[str, Any]: api_key = _require_process_api_key() stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") graph_id = args.graph_id or f"mirofish_zep_deep_{stamp}" builder = GraphBuilderService(api_key=api_key) client = builder.client created = False updater: ZepGraphMemoryUpdater | None = None updater_started = False updater_stop_attempted = False updater_drained = False result: dict[str, Any] = { "graph_id": graph_id, "graph_retained": args.keep_graph, "sdk_flow": "zep-cloud 3.25 standalone graph + current Batch API", } print(f"[zep-deep] graph_id={graph_id}", flush=True) try: builder.create_graph("MiroFish Zep Cloud Deep Validation", graph_id=graph_id) created = True print("[zep-deep] graph created", flush=True) # Production safety net: empty LLM attributes must become a valid # fallback property before reaching Zep Cloud. ontology_probe = {"entity_types": [{"name": "ProbeEntity", "attributes": []}], "edge_types": []} try: builder.set_ontology(graph_id, ontology_probe) result["empty_attribute_ontology_probe"] = "accepted_after_mirofish_normalization" except Exception as error: result["empty_attribute_ontology_probe"] = { "error_type": type(error).__name__, "status_code": _status_code(error), } raise builder.set_ontology(graph_id, ONTOLOGY) print("[zep-deep] ontology set", flush=True) operation_id = f"deep-validation-{stamp}" batch = client.batch.create( metadata={ "mirofish_operation_id": operation_id, "graph_id": graph_id, "suite": "zep_deep_validation", } ) batch_id = batch.batch_id if not batch_id: raise RuntimeError("batch.create returned no batch_id") result["batch_id"] = batch_id added_details: list[Any] = [] batch_items = [ _episode_to_batch_item(graph_id, item, index) for index, item in enumerate(BASELINE_EPISODES) ] for start in range(0, len(batch_items), 4): added_details.extend( client.batch.add(batch_id=batch_id, items=batch_items[start : start + 4]) ) client.batch.process(batch_id=batch_id) print(f"[zep-deep] batch submitted items={len(batch_items)}", flush=True) submission = BatchSubmission( batch_id=batch_id, operation_id=operation_id, episode_uuids=[_uuid(item) for item in added_details if _uuid(item)], item_count=len(batch_items), ) baseline_episode_uuids = builder._wait_for_batch(submission, timeout=args.timeout) listed_items, batch_pages = _list_batch_items(client, batch_id, page_size=3) result["batch"] = { "status": client.batch.get(batch_id=batch_id).status, "item_count": len(listed_items), "item_pages_at_size_3": batch_pages, "episode_uuids": baseline_episode_uuids, } print(f"[zep-deep] batch completed pages={batch_pages}", flush=True) baseline_nodes = fetch_all_nodes(client, graph_id, page_size=2) baseline_edges = fetch_all_edges(client, graph_id, page_size=2) raw_nodes, node_pages = _raw_pages( client.graph.node.with_raw_response.get_by_graph_id, graph_id, page_size=2 ) raw_edges, edge_pages = _raw_pages( client.graph.edge.with_raw_response.get_by_graph_id, graph_id, page_size=2 ) if {_uuid(item) for item in baseline_nodes} != {_uuid(item) for item in raw_nodes}: raise AssertionError("production node pagination did not match raw cursor traversal") if {_uuid(item) for item in baseline_edges} != {_uuid(item) for item in raw_edges}: raise AssertionError("production edge pagination did not match raw cursor traversal") if len(baseline_nodes) <= 2 or len(baseline_edges) <= 2: raise AssertionError("the corpus did not produce enough artifacts to exercise pagination") baseline_names = {_uuid(node): node.name for node in baseline_nodes} baseline_ceo = client.graph.search( graph_id=graph_id, query="截至2026年4月底,谁担任澜舟科技首席执行官?", scope="edges", reranker="cross_encoder", limit=10, ) result["baseline"] = { "node_count": len(baseline_nodes), "edge_count": len(baseline_edges), "node_pages_at_size_2": node_pages, "edge_pages_at_size_2": edge_pages, "invalidated_edge_count": sum(bool(edge.invalid_at) for edge in baseline_edges), "ceo_search": _search_view(baseline_ceo, baseline_names), } print( f"[zep-deep] baseline nodes={len(baseline_nodes)} edges={len(baseline_edges)}", flush=True, ) update_episode_uuids = [ _add_and_wait(client, graph_id, item, args.timeout) for item in TEMPORAL_UPDATES ] result["temporal_update_episode_uuids"] = update_episode_uuids print(f"[zep-deep] temporal updates processed={len(update_episode_uuids)}", flush=True) updater = ZepGraphMemoryUpdater( graph_id=graph_id, api_key=api_key, simulation_id=f"zep-deep-{stamp}", ) updater_started = True updater.start() for activity in _activities(): updater.add_activity(activity) updater_stop_attempted = True updater.stop() updater_drained = True updater_stats = updater.get_stats() if updater_stats["items_sent"] != 5 or updater_stats["pending_episode_count"] != 0: raise AssertionError(f"unexpected MiroFish updater stats: {updater_stats}") result["mirofish_updater"] = updater_stats print("[zep-deep] MiroFish updater processed 5 mock activities", flush=True) final_nodes = fetch_all_nodes(client, graph_id, page_size=2) final_edges = fetch_all_edges(client, graph_id, page_size=2) final_names = {_uuid(node): node.name for node in final_nodes} invalidated = [edge for edge in final_edges if edge.invalid_at] expired = [edge for edge in final_edges if edge.expired_at] edge_search = client.graph.search( graph_id=graph_id, query="澜舟科技当前首席执行官、当前总部以及与海岳能源的当前合作关系是什么?", scope="edges", reranker="cross_encoder", limit=20, ) node_search = client.graph.search( graph_id=graph_id, query="澜舟科技管理层人物", scope="nodes", reranker="rrf", limit=10, search_filters=SearchFilters(node_labels=["Person"]), ) typed_edge_search = client.graph.search( graph_id=graph_id, query="公司管理职位发生了什么变化?", scope="edges", reranker="rrf", limit=10, search_filters=SearchFilters(edge_types=["HOLDS_ROLE_AT"]), ) auto_search = client.graph.search( graph_id=graph_id, query="总结澜舟科技最新的管理层、总部、产品与海岳能源关系。", scope="auto", max_characters=3000, return_raw_results=True, ) episode_search = client.graph.search( graph_id=graph_id, query="海岳能源为什么仍是客户但不再是联合部署伙伴?", scope="episodes", reranker="rrf", limit=10, ) selected_node = next( ( node for node in final_nodes if node.name == "澜舟科技" and "Company" in (node.labels or []) ), max( final_nodes, key=lambda node: sum( edge.source_node_uuid == _uuid(node) or edge.target_node_uuid == _uuid(node) for edge in final_edges ), ), ) sdk_node_edges = client.graph.node.get_edges(node_uuid=_uuid(selected_node)) complete_node_edges = [ edge for edge in final_edges if edge.source_node_uuid == _uuid(selected_node) or edge.target_node_uuid == _uuid(selected_node) ] entity_context = ZepEntityReader(api_key=api_key).get_entity_with_context( graph_id, _uuid(selected_node), ) recent_episodes = client.graph.episode.get_by_graph_id(graph_id=graph_id, lastn=50) episode_list = getattr(recent_episodes, "episodes", None) or [] custom_labels = sorted( {label for node in final_nodes for label in (node.labels or []) if label != "Entity"} ) custom_edge_names = sorted( {edge.name for edge in final_edges if edge.name in {item["name"] for item in ONTOLOGY["edge_types"]}} ) result["final"] = { "node_count": len(final_nodes), "edge_count": len(final_edges), "invalidated_edge_count": len(invalidated), "expired_edge_count": len(expired), "custom_labels": custom_labels, "custom_edge_names": custom_edge_names, "nodes": [_node_view(node) for node in final_nodes], "invalidated_facts": [_edge_view(edge, final_names) for edge in invalidated], "active_facts": [ _edge_view(edge, final_names) for edge in final_edges if not edge.invalid_at and not edge.expired_at ], "selected_node": _node_view(selected_node), "sdk_node_edge_count": len(sdk_node_edges), "complete_node_edge_count": len(complete_node_edges), "entity_reader_context_edge_count": len(entity_context.related_edges), "recent_episode_count": len(episode_list), } result["searches"] = { "current_state_edges": _search_view(edge_search, final_names), "person_nodes": _search_view(node_search, final_names), "typed_role_edges": _search_view(typed_edge_search, final_names), "auto_context": _search_view(auto_search, final_names), "partnership_episodes": _search_view(episode_search, final_names), } if len(entity_context.related_edges) != len(complete_node_edges): raise AssertionError( "MiroFish entity context omitted incoming or outgoing node edges" ) result["runtime_assertions"] = { "edge_search_call_completed": edge_search is not None, "node_search_call_completed": node_search is not None, "typed_edge_search_call_completed": typed_edge_search is not None, "auto_search_call_completed": auto_search is not None, "episode_search_call_completed": episode_search is not None, "node_detail_has_all_incoming_and_outgoing_edges": True, "sdk_node_endpoint_omits_incoming_edges": ( len(sdk_node_edges) < len(complete_node_edges) ), # The following values are observations only. Zep's extraction and # retrieval quality are not runtime acceptance criteria. "search_result_counts": { "edges": len(edge_search.edges or []), "nodes": len(node_search.nodes or []), "typed_edges": len(typed_edge_search.edges or []), "episodes": len(episode_search.episodes or []), }, "custom_entity_labels_observed": bool(custom_labels), "custom_edge_names_observed": bool(custom_edge_names), "temporal_invalidation_observed": bool(invalidated), } return result except Exception as error: result["failure"] = { "type": type(error).__name__, "status_code": _status_code(error), "message": str(error)[:500], } raise finally: if updater is not None and updater_started and not updater_stop_attempted: updater_stop_attempted = True updater_drained, updater_cleanup_error = _drain_updater_after_failure( updater, started=updater_started, stop_attempted=False, ) if updater_cleanup_error is not None: result["updater_cleanup_error"] = { "type": type(updater_cleanup_error).__name__, "message": str(updater_cleanup_error)[:500], } print( "[zep-deep] updater drain failed; graph will be retained " f"type={type(updater_cleanup_error).__name__}", flush=True, ) had_primary_failure = "failure" in result try: cleanup = _cleanup_graph( client, graph_id, created=created, keep_graph=args.keep_graph, updater_started=updater_started, updater_drained=updater_drained, ) except Exception as cleanup_error: result["cleanup"] = { "graph_deleted": False, "graph_retained": True, "reason": "graph_delete_failed", "error_type": type(cleanup_error).__name__, "status_code": _status_code(cleanup_error), } result["graph_retained"] = True print( "[zep-deep] graph cleanup failed; graph retained " f"graph_id={graph_id} type={type(cleanup_error).__name__}", flush=True, ) if not had_primary_failure: result["failure"] = { "type": type(cleanup_error).__name__, "status_code": _status_code(cleanup_error), "message": str(cleanup_error)[:500], } print(json.dumps(result, ensure_ascii=False, indent=2), flush=True) raise else: result["cleanup"] = cleanup result["graph_retained"] = cleanup["graph_retained"] if cleanup["graph_deleted"]: action = "deleted" elif cleanup["graph_retained"]: action = "retained" else: action = "not-created" print( f"[zep-deep] graph {action} graph_id={graph_id} " f"reason={cleanup['reason']}", flush=True, ) if had_primary_failure: print(json.dumps(result, ensure_ascii=False, indent=2), flush=True) def _compact_result(result: dict[str, Any]) -> dict[str, Any]: """Keep terminal output focused on runtime/API-contract evidence.""" baseline = result.get("baseline", {}) final = result.get("final", {}) return { "graph_id": result.get("graph_id"), "graph_retained": result.get("graph_retained"), "cleanup": result.get("cleanup"), "sdk_flow": result.get("sdk_flow"), "empty_attribute_ontology_probe": result.get( "empty_attribute_ontology_probe" ), "batch_id": result.get("batch_id"), "batch": result.get("batch"), "baseline": { key: baseline.get(key) for key in ( "node_count", "edge_count", "node_pages_at_size_2", "edge_pages_at_size_2", "invalidated_edge_count", ) }, "temporal_update_episode_count": len( result.get("temporal_update_episode_uuids", []) ), "mirofish_updater": result.get("mirofish_updater"), "final": { key: final.get(key) for key in ( "node_count", "edge_count", "invalidated_edge_count", "expired_edge_count", "custom_labels", "custom_edge_names", "sdk_node_edge_count", "complete_node_edge_count", "entity_reader_context_edge_count", "recent_episode_count", ) }, "runtime_assertions": result.get("runtime_assertions"), } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--graph-id", help="Use an explicit standalone graph ID") parser.add_argument( "--keep-graph", action="store_true", help="Retain the graph for manual inspection after validation", ) parser.add_argument( "--timeout", type=int, default=900, help="Maximum seconds to wait for each ingestion stage", ) parser.add_argument( "--full-output", action="store_true", help="Print every node, edge, and search result instead of a runtime summary", ) args = parser.parse_args() try: result = run(args) except Exception as error: print( f"[zep-deep] FAILED type={type(error).__name__} status={_status_code(error)}", file=sys.stderr, ) return 1 output = result if args.full_output else _compact_result(result) print(json.dumps(output, ensure_ascii=False, indent=2), flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())