查到价后把币种、单位、数量、单价、备注写入报价单,避免销售只看到光秃秃的金额。 Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
10 KiB
Python
269 lines
10 KiB
Python
"""
|
||
基础框架联合自检(不连真实 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())
|