""" 基础框架联合自检(不连真实 PG/外网模型也可跑)。 覆盖:迁移、正式合同、ledger、llm、DeepSeek A 解析、LibreOffice 探测、 识别/存档入队、routing、storage。 用法: set REDIS_BACKEND=memory set CHECKPOINT_BACKEND=memory python -m agent.foundation_selftest """ from __future__ import annotations import logging import os import sys logger = logging.getLogger(__name__) def main() -> int: logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") os.environ["REDIS_BACKEND"] = "memory" os.environ.setdefault("YTD_ENV", "test") os.environ.setdefault("CHECKPOINT_BACKEND", "memory") os.environ.setdefault("REDIS_KEY_PREFIX", "inquiry_robot:") os.environ["LLM_DATA_USAGE_CONFIRMED"] = "false" os.environ["LLM_ALLOW_NETWORK"] = "false" from agent.config import get_settings from agent.jobs.archive import enqueue_archive from agent.jobs.libreoffice import LibreOfficeConverter from agent.jobs.recognition import enqueue_recognition from agent.ledger import LedgerClient from agent.llm import LlmGateway, LlmMode from agent.redis_coord import shutdown_redis_runtime from agent.routing import IntentRouter, ordered_actions_tool_definition, parse_ordered_actions_arguments from agent.runtime_db import assert_safe_database, list_migration_files from agent.schema import load_contract, require_formal_contract from agent.storage import ObjectStorageClient get_settings.cache_clear() settings = get_settings() files = list_migration_files() assert "001_runtime_core_v1.sql" in files, files assert "002_dead_letter_v1.sql" in files, files logger.info("OK: migrations %s", files) try: assert_safe_database("postgresql://u:p@127.0.0.1:5432/ytd_runtime") logger.error("应拒绝 ytd_runtime") return 1 except RuntimeError: logger.info("OK: 拒绝现网库名 ytd_runtime") contract = load_contract("inquiry-required-fields-v1") assert contract["_meta"]["is_placeholder"] is False, contract["_meta"] formal = require_formal_contract("inquiry-required-fields-v1") assert formal.get("contract_id") or formal.get("schema_version") is not None tms = require_formal_contract("tms-dictionary-projection-v2") assert tms["_meta"]["is_placeholder"] is False logger.info("OK: 正式合同已加载 fields+tms") tool = ordered_actions_tool_definition() assert tool["function"]["name"] == "route_decision" digest = "a" * 64 parsed = parse_ordered_actions_arguments( { "decision_id": "D-1", "intent": "ordinary_text_inquiry", "ordered_action": { "action_type": "ORDINARY_TEXT_INQUIRY", "order": 1, "target": {"work_order_no": ""}, "evidence": {"source_ref": "inbound:u1", "content_digest": digest}, }, } ) assert parsed.intent == "ordinary_text_inquiry" logger.info("OK: ordered_actions 解析") ledger = LedgerClient.from_settings(settings) assert "8180" in ledger.base_url or ledger.base_url logger.info("OK: ledger client base=%s", ledger.base_url) ping = ledger.ping() logger.info("ledger.ping ok=%s status=%s", ping.ok, ping.status_code) ledger.close() gw = LlmGateway.from_settings(settings) enq = gw.enqueue( mode=LlmMode.ORDERED_ACTIONS, payload={"text": "hi"}, idempotency_key="foundation-1", ) assert enq.accepted, enq stub = gw.invoke_stub(mode=LlmMode.EXTRACT_FIELDS, payload={"a": 1}) assert stub.get("stub") is True assert stub.get("error") == "extract_fields_shell_not_implemented" from agent.llm import list_mode_shells assert "vision" in list_mode_shells() logger.info("OK: llm enqueue/mode shells") decision = IntentRouter().route_stub(text="询价", sender_id="u1") assert decision.intent == "ordinary_text_inquiry", decision.intent assert IntentRouter().route(text="询价", sender_id="").intent == "reject_no_sender" logger.info("OK: DeepSeek A stub 路由") from agent.channel.clarification import issue_token, validate_token from agent.channel.queue import reset_message_store_for_tests from agent.channel.wecom.models import InboundMessage reset_message_store_for_tests() os.environ["MESSAGE_STORE_BACKEND"] = "memory" get_settings.cache_clear() reset_message_store_for_tests() from agent.channel.queue import get_message_store store = get_message_store() assert store.stats().get("backend") == "memory" msg = InboundMessage(sender_id="u1", message_id="m-foundation-1", content="hello") ok, iid = store.enqueue_inbound(msg) assert ok, iid claimed = store.claim_inbound() assert claimed is not None store.complete_inbound(claimed.id) logger.info("OK: memory inbox claim") tok = issue_token( thread_id="t1", wait_version=1, sender_id="u1", allowed_actions=["submit_clarify"], ) rec = validate_token(tok, action="submit_clarify", consume=True) assert rec is not None and rec.thread_id == "t1" assert validate_token(tok) is None logger.info("OK: clarification token") from agent.graph import GraphRuntime, shutdown_graph_runtime from agent.graph.wake import WorkerWakePayload from agent.jobs.wake_consumer import claim_and_apply_wake, enqueue_wake, wake_stream from agent.llm import LlmHttpClient from agent.redis_coord import get_redis_runtime os.environ["CHECKPOINT_BACKEND"] = "memory" get_settings.cache_clear() gr = GraphRuntime.create(get_settings()) started = gr.start_thread(sender_id="u1") tid = started["thread_id"] rr = get_redis_runtime(get_settings()) entry = enqueue_wake( rr.client, WorkerWakePayload( thread_id=tid, inquiry_no="F-1", quote_version=1, wait_version=0, kind="recognition_done", ), ) assert entry q = wake_stream(rr.client) assert claim_and_apply_wake(queue=q, graph=gr, consumer="selftest") logger.info("OK: wake enqueue/apply") http_llm = LlmHttpClient.from_settings(get_settings()) no_net = http_llm.chat(mode="ordered_actions", messages=[{"role": "user", "content": "hi"}]) assert no_net.ok is False and no_net.error == "llm_data_usage_not_confirmed" logger.info("OK: llm http 拒绝未确认数据用途") recog = enqueue_recognition( kind="attachment_text", payload={"object_key": "stub"}, idempotency_key="recog-1", redis=rr, ) assert recog.accepted, recog arch = enqueue_archive(payload={"msg_id": "m1"}, idempotency_key="arch-1", redis=rr) assert arch.accepted, arch logger.info("OK: recognition/archive enqueue") lo = LibreOfficeConverter.from_settings(get_settings()) logger.info("OK: LibreOffice available=%s path=%s", lo.available(), lo.executable()) gr.close() shutdown_graph_runtime() storage = ObjectStorageClient.from_settings(settings) put_ok = storage.put_bytes_stub(key="x", data=b"1") assert put_ok is storage.configured() logger.info("OK: storage stub configured=%s put_ok=%s", storage.configured(), put_ok) from agent.jobs.dead_letter import enqueue_dead_letter from agent.jobs.pg_job import create_job_store from agent.ledger.api_shell import LedgerApiShell from agent.policy import assert_has_sender_id, policy_package_ready from agent.quote_templates import list_template_registry from agent.routing.dispatch import HANDLER_REGISTRY, dispatch_inbound from agent.runtime_db import SessionRecord, create_session_store, create_suspend_store from agent.runtime_db.suspend_store import SuspendRecord from agent.schema import validate_required_fields_shell from agent.storage.s3_client import S3ObjectStorage assert policy_package_ready() assert assert_has_sender_id("u1")["ok"] is True assert assert_has_sender_id("")["ok"] is False sess = create_session_store(backend="memory") sess.upsert(SessionRecord(thread_id="t-shell", sender_id="u1")) assert sess.get("t-shell") is not None sus = create_suspend_store(backend="memory") sus.open_wait(SuspendRecord(thread_id="t-shell", wait_version=1, pause_name="pause_parse_clarify")) assert sus.get("t-shell", 1) is not None assert sus.close_wait("t-shell", 1) is True jobs = create_job_store(backend="memory") ok_j, jid = jobs.enqueue(job_type="shell", payload={"x": 1}, idempotency_key="j1") assert ok_j, jid claimed_j = jobs.claim(worker_id="selftest") assert claimed_j is not None jobs.complete(claimed_j.id) dl = enqueue_dead_letter(source="selftest", error="demo", job_type="shell") assert dl assert "ordinary_text_inquiry" in HANDLER_REGISTRY d2 = IntentRouter().route_stub(text="x", sender_id="u1") msg2 = InboundMessage(sender_id="u1", message_id="m-dispatch", content="x") # dispatch 可能写 outbox(echo);仅验证查表调用不抛 dispatch_inbound(msg2, d2) shell_api = LedgerApiShell(LedgerClient.from_settings(get_settings())) assert shell_api.create_ticket(payload={}, idempotency_key="x").status_code == 501 from agent.quote_templates import list_templates_from_ledger lt = list_templates_from_ledger(LedgerClient.from_settings(get_settings())) # 主账可达时返回 2xx;不可达时 status_code=0 + error,不再假装 501 assert isinstance(lt.status_code, int) assert lt.ok or lt.error or lt.data is not None logger.info( "OK: list_quote_templates status=%s ok=%s err=%s", lt.status_code, lt.ok, (lt.error or "")[:80], ) vf = validate_required_fields_shell(facts={}, require_formal=True) assert vf.get("implemented") is True _ = list_template_registry() s3 = S3ObjectStorage.from_settings(get_settings(), allow_network=False) put_s3 = s3.put_bytes(key="k", data=b"1") assert put_s3.get("dry_run") is True or put_s3.get("error") in { "not_configured", "boto3_not_installed", } logger.info("OK: session/suspend/job/dead_letter/dispatch/ledger/schema/s3 shells") shutdown_redis_runtime() logger.info("基础框架联合自检通过") return 0 if __name__ == "__main__": sys.exit(main())