Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/0206huang_docs.json
T

1 line
32 KiB
JSON

{"repo": "0206huang/docs", "files": {"backend/scripts/demo_live.py": {"e711c755be62242b5ac436e70ed101415c42364c": {"t": 1785746620.2781901, "n": 5894, "h": "d6e6c0c0c5a06133", "c": "\"\"\"\nLive end-to-end pipeline demo against the real DeepSeek API.\n\nRuns the SAME production functions the worker uses — sensitive-word gate,\nLLM classification, RAG retrieval (local lexical embedding), route decision,\nand reply generation — over a handful of realistic inbound emails, printing\neach stage. No database required.\n\nUsage (inside the app image, backend/ mounted at /app):\n DEEPSEEK_KEY=sk-... python scripts/demo_live.py\n\"\"\"\nimport asyncio\nimport os\nimport sys\n\nfrom app.services.llm.openai_provider import DeepSeekProvider\nfrom app.services.llm.local_embed import LocalHashEmbedProvider\nfrom app.services.pipeline.classifier import classify_email\nfrom app.services.pipeline.reply_generator import generate_reply\nfrom app.services.pipeline.router import NO_REPLY_TYPES, decide_strategy\nfrom app.services.pipeline.sensitive import check_sensitive_words\n\n# ── Demo tenant config ──────────────────────────────────────────────────────\nSENSITIVE_WORDS = [\"律师\", \"诉讼\", \"赔偿\", \"投诉\", \"起诉\"]\n\nKB = [\n {\"content\": \"我们的标准产品价格为每件100元。批量采购500件以上享8折,1000件以上享7折优惠。\"},\n {\"content\": \"所有订单在付款后2个工作日内发货,默认使用顺丰快递,偏远地区3-5天送达。\"},\n {\"content\": \"支持7天无理由退货,产品需保持完好未使用。质量问题15天内包退换,运费由我方承担。\"},\n {\"content\": \"产品图册、规格书和报价单可作为邮件附件发送。如需纸质版样册可申请邮寄。\"},\n]\n\nEMAILS = [\n {\n \"from_addr\": \"zhang@acme-corp.com\", \"from_name\": \"张先生\",\n \"subject\": \"批量采购价格咨询\",\n \"body\": \"你好,我们公司想批量采购你们的产品,预计1000件左右,请问价格怎么算?有没有折扣?期待回复。\",\n },\n {\n \"from_addr\": \"customer@gmail.com\", \"from_name\": \"李女士\",\n \"subject\": \"产品质量问题\",\n \"body\": \"我上周收到的产品有破损,包装也不完整,非常失望。请问怎么处理退换货?\",\n },\n {\n \"from_addr\": \"angry@client.com\", \"from_name\": \"王总\",\n \"subject\": \"严重警告\",\n \"body\": \"如果这批货的问题再不解决,我将委托律师提起诉讼,并要求全额赔偿!\",\n },\n {\n \"from_addr\": \"buyer@overseas.com\", \"from_name\": \"John Smith\",\n \"subject\": \"Product catalog request\",\n \"body\": \"Hello, could you please send me your product catalog and latest pricing? We are evaluating suppliers. Thanks.\",\n },\n {\n \"from_addr\": \"promo@spam-ads.net\", \"from_name\": None,\n \"subject\": \"🎉 恭喜您中奖100万\",\n \"body\": \"您已被抽中为幸运用户!点击链接立即领取100万现金大奖,手慢无!\",\n },\n]\n\nTYPE_CN = {\n \"customer_inquiry\": \"客户咨询\", \"quote_request\": \"报价请求\", \"material_request\": \"资料索取\",\n \"complaint\": \"投诉\", \"payment_reminder\": \"催付款\", \"order_confirm\": \"订单确认\",\n \"supplier\": \"供应商\", \"resume\": \"简历\", \"partnership\": \"合作邀约\", \"legal\": \"法律函件\",\n \"spam\": \"垃圾邮件\", \"ad_no_reply\": \"广告勿回\", \"other\": \"其他\",\n}\nDECISION_CN = {\n \"auto_send\": \"✅ 自动发送\", \"draft_review\": \"📝 草稿待审\", \"human_only\": \"👤 转人工\", \"skip\": \"⏭️ 跳过\",\n}\n\n\ndef dot(a, b):\n return sum(x * y for x, y in zip(a, b))\n\n\nasync def retrieve(embed, email_type, query, top_k=2):\n if email_type in NO_REPLY_TYPES:\n return []\n kb_vecs = (await embed.embed([k[\"content\"] for k in KB])).embeddings\n q_vec = (await embed.embed([query])).embeddings[0]\n scored = sorted(\n ({\"content\": KB[i][\"content\"], \"score\": dot(q_vec, kb_vecs[i])} for i in range(len(KB))),\n key=lambda x: x[\"score\"], reverse=True,\n )\n floor = getattr(embed, \"embed_min_score\", 0.1)\n return [c for c in scored[:top_k] if c[\"score\"] >= floor]\n\n\nasync def process(llm, embed, i, email):\n print(\"\\n\" + \"═\" * 72)\n print(f\"📧 邮件 #{i} 来自 {email['from_name'] or '?'} <{email['from_addr']}>\")\n print(f\" 主题: {email['subject']}\")\n print(f\" 正文: {email['body'][:60]}...\")\n print(\"─\" * 72)\n\n full_text = f\"{email['subject']} {email['body']}\"\n\n # 1. Sensitive-word gate (runs before any LLM call)\n matched = check_sensitive_words(full_text, SENSITIVE_WORDS)\n if matched:\n print(f\"🚨 敏感词命中: {matched} → 强制转人工 + 生成工单 + 告警管理员\")\n print(f\" {DECISION_CN['human_only']} (未调用 LLM,直接拦截)\")\n return\n\n # 2. Real DeepSeek classification\n cls = await classify_email(llm, email[\"subject\"], email[\"body\"], email[\"from_addr\"])\n etype, lang, urg = cls[\"email_type\"], cls.get(\"language\", \"?\"), cls.get(\"urgency\", 1)\n print(f\"🧠 DeepSeek 分类: {TYPE_CN.get(etype, etype)} ({etype}) · 语言={lang} · 紧急度={urg}\"\n f\" · tokens={cls.get('prompt_tokens',0)}+{cls.get('completion_tokens',0)}\")\n\n # 3. RAG retrieval (local lexical embedding)\n hits = await retrieve(embed, etype, full_text)\n if hits:\n for h in hits:\n print(f\"📚 知识库命中 (score={h['score']:.3f}): {h['content'][:40]}...\")\n else:\n print(\"📚 知识库未命中\")\n rag_found = len(hits) > 0\n\n # 4. Route decision\n decision = decide_strategy(etype, has_sensitive=False, rag_found=rag_found, tenant_strategy=None)\n print(f\"🔀 路由决策: {DECISION_CN.get(decision, decision)}\")\n\n # 5. Reply generation (real DeepSeek), for auto_send / draft_review\n if decision in (\"auto_send\", \"draft_review\"):\n reply, model = await generate_reply(\n llm, email[\"subject\"], email[\"body\"], email[\"from_addr\"],\n language=lang, tone=\"business\", context_chunks=hits,\n )\n print(f\"\\n✍️ DeepSeek 生成回复 ({model}):\")\n print(\" \" + reply.replace(\"\\n\", \"\\n \"))\n elif decision == \"human_only\":\n print(\" → 无知识库依据或策略要求,转人工队列\")\n elif decision == \"skip\":\n print(\" → 垃圾/广告邮件,自动忽略不回复\")\n\n\nasync def main():\n key = os.environ.get(\"DEEPSEEK_KEY\")\n if not key:\n print(\"ERROR: set DEEPSEEK_KEY env var\", file=sys.stderr)\n sys.exit(1)\n\n llm = DeepSeekProvider(api_key=key)\n embed = LocalHashEmbedProvider()\n\n print(\"\\n🚀 企业邮件 AI · 真实链路演示 (DeepSeek + 本地向量检索)\")\n print(f\" 知识库: {len(KB)} 条 · 敏感词: {len(SENSITIVE_WORDS)} 个 · 测试邮件: {len(EMAILS)} 封\")\n\n for i, email in enumerate(EMAILS, 1):\n try:\n await process(llm, embed, i, email)\n except Exception as e:\n print(f\"❌ 处理失败: {type(e).__name__}: {e}\")\n\n print(\"\\n\" + \"═\" * 72)\n print(\"✅ 演示完成 — 分类与回复均由真实 DeepSeek 生成,检索用本地向量近似\")\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n"}, "bc4fabf2f7e0588b6938b9515f9484b65a46976c": {"t": 1785766348.8716476, "n": 5894, "h": "d6e6c0c0c5a06133", "c": "\"\"\"\nLive end-to-end pipeline demo against the real DeepSeek API.\n\nRuns the SAME production functions the worker uses — sensitive-word gate,\nLLM classification, RAG retrieval (local lexical embedding), route decision,\nand reply generation — over a handful of realistic inbound emails, printing\neach stage. No database required.\n\nUsage (inside the app image, backend/ mounted at /app):\n DEEPSEEK_KEY=sk-... python scripts/demo_live.py\n\"\"\"\nimport asyncio\nimport os\nimport sys\n\nfrom app.services.llm.openai_provider import DeepSeekProvider\nfrom app.services.llm.local_embed import LocalHashEmbedProvider\nfrom app.services.pipeline.classifier import classify_email\nfrom app.services.pipeline.reply_generator import generate_reply\nfrom app.services.pipeline.router import NO_REPLY_TYPES, decide_strategy\nfrom app.services.pipeline.sensitive import check_sensitive_words\n\n# ── Demo tenant config ──────────────────────────────────────────────────────\nSENSITIVE_WORDS = [\"律师\", \"诉讼\", \"赔偿\", \"投诉\", \"起诉\"]\n\nKB = [\n {\"content\": \"我们的标准产品价格为每件100元。批量采购500件以上享8折,1000件以上享7折优惠。\"},\n {\"content\": \"所有订单在付款后2个工作日内发货,默认使用顺丰快递,偏远地区3-5天送达。\"},\n {\"content\": \"支持7天无理由退货,产品需保持完好未使用。质量问题15天内包退换,运费由我方承担。\"},\n {\"content\": \"产品图册、规格书和报价单可作为邮件附件发送。如需纸质版样册可申请邮寄。\"},\n]\n\nEMAILS = [\n {\n \"from_addr\": \"zhang@acme-corp.com\", \"from_name\": \"张先生\",\n \"subject\": \"批量采购价格咨询\",\n \"body\": \"你好,我们公司想批量采购你们的产品,预计1000件左右,请问价格怎么算?有没有折扣?期待回复。\",\n },\n {\n \"from_addr\": \"customer@gmail.com\", \"from_name\": \"李女士\",\n \"subject\": \"产品质量问题\",\n \"body\": \"我上周收到的产品有破损,包装也不完整,非常失望。请问怎么处理退换货?\",\n },\n {\n \"from_addr\": \"angry@client.com\", \"from_name\": \"王总\",\n \"subject\": \"严重警告\",\n \"body\": \"如果这批货的问题再不解决,我将委托律师提起诉讼,并要求全额赔偿!\",\n },\n {\n \"from_addr\": \"buyer@overseas.com\", \"from_name\": \"John Smith\",\n \"subject\": \"Product catalog request\",\n \"body\": \"Hello, could you please send me your product catalog and latest pricing? We are evaluating suppliers. Thanks.\",\n },\n {\n \"from_addr\": \"promo@spam-ads.net\", \"from_name\": None,\n \"subject\": \"🎉 恭喜您中奖100万\",\n \"body\": \"您已被抽中为幸运用户!点击链接立即领取100万现金大奖,手慢无!\",\n },\n]\n\nTYPE_CN = {\n \"customer_inquiry\": \"客户咨询\", \"quote_request\": \"报价请求\", \"material_request\": \"资料索取\",\n \"complaint\": \"投诉\", \"payment_reminder\": \"催付款\", \"order_confirm\": \"订单确认\",\n \"supplier\": \"供应商\", \"resume\": \"简历\", \"partnership\": \"合作邀约\", \"legal\": \"法律函件\",\n \"spam\": \"垃圾邮件\", \"ad_no_reply\": \"广告勿回\", \"other\": \"其他\",\n}\nDECISION_CN = {\n \"auto_send\": \"✅ 自动发送\", \"draft_review\": \"📝 草稿待审\", \"human_only\": \"👤 转人工\", \"skip\": \"⏭️ 跳过\",\n}\n\n\ndef dot(a, b):\n return sum(x * y for x, y in zip(a, b))\n\n\nasync def retrieve(embed, email_type, query, top_k=2):\n if email_type in NO_REPLY_TYPES:\n return []\n kb_vecs = (await embed.embed([k[\"content\"] for k in KB])).embeddings\n q_vec = (await embed.embed([query])).embeddings[0]\n scored = sorted(\n ({\"content\": KB[i][\"content\"], \"score\": dot(q_vec, kb_vecs[i])} for i in range(len(KB))),\n key=lambda x: x[\"score\"], reverse=True,\n )\n floor = getattr(embed, \"embed_min_score\", 0.1)\n return [c for c in scored[:top_k] if c[\"score\"] >= floor]\n\n\nasync def process(llm, embed, i, email):\n print(\"\\n\" + \"═\" * 72)\n print(f\"📧 邮件 #{i} 来自 {email['from_name'] or '?'} <{email['from_addr']}>\")\n print(f\" 主题: {email['subject']}\")\n print(f\" 正文: {email['body'][:60]}...\")\n print(\"─\" * 72)\n\n full_text = f\"{email['subject']} {email['body']}\"\n\n # 1. Sensitive-word gate (runs before any LLM call)\n matched = check_sensitive_words(full_text, SENSITIVE_WORDS)\n if matched:\n print(f\"🚨 敏感词命中: {matched} → 强制转人工 + 生成工单 + 告警管理员\")\n print(f\" {DECISION_CN['human_only']} (未调用 LLM,直接拦截)\")\n return\n\n # 2. Real DeepSeek classification\n cls = await classify_email(llm, email[\"subject\"], email[\"body\"], email[\"from_addr\"])\n etype, lang, urg = cls[\"email_type\"], cls.get(\"language\", \"?\"), cls.get(\"urgency\", 1)\n print(f\"🧠 DeepSeek 分类: {TYPE_CN.get(etype, etype)} ({etype}) · 语言={lang} · 紧急度={urg}\"\n f\" · tokens={cls.get('prompt_tokens',0)}+{cls.get('completion_tokens',0)}\")\n\n # 3. RAG retrieval (local lexical embedding)\n hits = await retrieve(embed, etype, full_text)\n if hits:\n for h in hits:\n print(f\"📚 知识库命中 (score={h['score']:.3f}): {h['content'][:40]}...\")\n else:\n print(\"📚 知识库未命中\")\n rag_found = len(hits) > 0\n\n # 4. Route decision\n decision = decide_strategy(etype, has_sensitive=False, rag_found=rag_found, tenant_strategy=None)\n print(f\"🔀 路由决策: {DECISION_CN.get(decision, decision)}\")\n\n # 5. Reply generation (real DeepSeek), for auto_send / draft_review\n if decision in (\"auto_send\", \"draft_review\"):\n reply, model = await generate_reply(\n llm, email[\"subject\"], email[\"body\"], email[\"from_addr\"],\n language=lang, tone=\"business\", context_chunks=hits,\n )\n print(f\"\\n✍️ DeepSeek 生成回复 ({model}):\")\n print(\" \" + reply.replace(\"\\n\", \"\\n \"))\n elif decision == \"human_only\":\n print(\" → 无知识库依据或策略要求,转人工队列\")\n elif decision == \"skip\":\n print(\" → 垃圾/广告邮件,自动忽略不回复\")\n\n\nasync def main():\n key = os.environ.get(\"DEEPSEEK_KEY\")\n if not key:\n print(\"ERROR: set DEEPSEEK_KEY env var\", file=sys.stderr)\n sys.exit(1)\n\n llm = DeepSeekProvider(api_key=key)\n embed = LocalHashEmbedProvider()\n\n print(\"\\n🚀 企业邮件 AI · 真实链路演示 (DeepSeek + 本地向量检索)\")\n print(f\" 知识库: {len(KB)} 条 · 敏感词: {len(SENSITIVE_WORDS)} 个 · 测试邮件: {len(EMAILS)} 封\")\n\n for i, email in enumerate(EMAILS, 1):\n try:\n await process(llm, embed, i, email)\n except Exception as e:\n print(f\"❌ 处理失败: {type(e).__name__}: {e}\")\n\n print(\"\\n\" + \"═\" * 72)\n print(\"✅ 演示完成 — 分类与回复均由真实 DeepSeek 生成,检索用本地向量近似\")\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n"}}, "backend/scripts/verify_e2e.py": {"e711c755be62242b5ac436e70ed101415c42364c": {"t": 1785746621.6294835, "n": 7419, "h": "8c8694e8846bb692", "c": "\"\"\"\nEnd-to-end production-path verification.\n\nProves the REAL runtime path — not a script reimplementation:\n inject inbound email → enqueue on ARQ/Redis → the worker container's\n process_email task → orchestrator → writes classification/reply/ticket/\n audit rows back to Postgres.\n\nSteps:\n 1. Configure the demo tenant's LLM = DeepSeek (encrypted).\n 2. Widen the FAQ KB group's email_types and RE-EMBED its chunks with the\n local embedder (seed stored zero-vector placeholders that can't match).\n 3. Force sales reply-types to draft_review so no fake SMTP send is attempted.\n 4. Inject a fresh inbound email on the sales account.\n 5. enqueue_process_email(...) → real worker picks it up.\n 6. Poll Postgres until the worker writes the classification, then print the\n full audit trail + generated reply / ticket.\n\nRun in a one-off container on the compose network (worker must run latest image):\n DEEPSEEK_KEY=sk-... python scripts/verify_e2e.py\n\"\"\"\nimport asyncio\nimport os\nimport sys\nimport uuid\nfrom datetime import datetime, timezone\n\nfrom sqlalchemy import select\n\nfrom app.core.security import encrypt_value\nfrom app.db.base import AsyncSessionLocal\nfrom app.db.models import (\n AuditLog, Email, EmailAccount, EmailClassification, EmailReply,\n EmailThread, EmailTypeStrategy, KBChunk, KBGroup, Ticket, Tenant,\n)\nfrom app.services.llm.local_embed import LocalHashEmbedProvider\n\nDEMO_DOMAIN = \"demo.emailai.local\"\nSALES_TYPES = [\"customer_inquiry\", \"quote_request\", \"material_request\", \"order_confirm\", \"complaint\", \"other\"]\n\nSTAGE_CN = {\n \"sensitive_blocked\": \"敏感词拦截\", \"classified\": \"分类完成\", \"rag_retrieved\": \"知识库检索\",\n \"reply_generated\": \"生成回复\", \"draft_created\": \"草稿待审\", \"sent\": \"已发送\",\n \"human_routed\": \"转人工\", \"skipped\": \"跳过\",\n}\n\n\nasync def prepare(db, key: str) -> tuple[uuid.UUID, uuid.UUID]:\n embed = LocalHashEmbedProvider()\n\n tenant = (await db.execute(select(Tenant).where(Tenant.domain == DEMO_DOMAIN))).scalar_one()\n\n # 1. LLM = DeepSeek (chat); embeddings fall back to local automatically\n s = dict(tenant.settings or {})\n s[\"llm_config\"] = {\"provider\": \"deepseek\", \"api_key_enc\": encrypt_value(key), \"model\": \"deepseek-chat\"}\n tenant.settings = s\n\n # 2. widen FAQ group types + re-embed all chunks with the real local embedder\n groups = (await db.execute(select(KBGroup).where(KBGroup.tenant_id == tenant.id))).scalars().all()\n for g in groups:\n if g.category == \"faq\" or \"产品\" in g.name:\n g.email_types = SALES_TYPES\n if \"sales\" not in (g.positioning or []):\n g.positioning = list(set((g.positioning or []) + [\"sales\"]))\n\n chunks = (await db.execute(select(KBChunk).where(KBChunk.tenant_id == tenant.id))).scalars().all()\n for c in chunks:\n c.embedding = (await embed.embed([c.content])).embeddings[0]\n print(f\" ✓ 重算 {len(chunks)} 条知识库 chunk 的本地向量\")\n\n # 3. force sales reply-types to draft_review (avoid fake-SMTP auto_send)\n strategies = (await db.execute(\n select(EmailTypeStrategy).where(EmailTypeStrategy.tenant_id == tenant.id)\n )).scalars().all()\n by_type = {st.email_type: st for st in strategies}\n for t in (\"customer_inquiry\", \"quote_request\", \"material_request\"):\n if t in by_type:\n by_type[t].send_strategy = \"draft_review\"\n\n account = (await db.execute(\n select(EmailAccount).where(EmailAccount.tenant_id == tenant.id, EmailAccount.positioning == \"sales\")\n )).scalar_one()\n\n await db.commit()\n return tenant.id, account.id\n\n\nasync def inject(db, tenant_id, account_id) -> str:\n account = (await db.execute(select(EmailAccount).where(EmailAccount.id == account_id))).scalar_one()\n thread = EmailThread(id=uuid.uuid4(), tenant_id=tenant_id, account_id=account_id,\n subject=\"产品规格与交货期咨询\", status=\"open\")\n db.add(thread)\n await db.flush()\n\n email = Email(\n id=uuid.uuid4(), tenant_id=tenant_id, account_id=account_id, thread_id=thread.id,\n message_id=f\"<e2e-{uuid.uuid4()}@buyer.test>\", direction=\"inbound\",\n from_addr=\"wang@acme-trading.com\", from_name=\"采购王经理\",\n to_addrs=[account.email_address], cc_addrs=[],\n subject=\"产品规格与交货期咨询\",\n body_text=\"你好,我们准备下单,请问贵公司产品有哪些规格?标准版和专业版有什么区别?\"\n \"另外交货期大概多久?希望尽快回复,谢谢。\",\n received_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),\n )\n db.add(email)\n await db.commit()\n return str(email.id)\n\n\nasync def main():\n key = os.environ.get(\"DEEPSEEK_KEY\")\n if not key:\n print(\"ERROR: set DEEPSEEK_KEY\", file=sys.stderr)\n sys.exit(1)\n\n print(\"\\n🔧 准备阶段\")\n async with AsyncSessionLocal() as db:\n tenant_id, account_id = await prepare(db, key)\n print(\" ✓ 配置 tenant LLM = DeepSeek,销售类型 → 草稿待审\")\n\n async with AsyncSessionLocal() as db:\n email_id = await inject(db, tenant_id, account_id)\n print(f\" ✓ 注入 inbound 邮件 email_id={email_id}\")\n\n # enqueue onto the REAL ARQ queue — the worker container consumes it\n from app.worker.tasks import enqueue_process_email\n await enqueue_process_email(email_id)\n print(\"\\n📨 已投递到 ARQ 队列,等待 worker 容器处理...\")\n\n # poll until worker writes the classification\n cls = None\n for i in range(30):\n await asyncio.sleep(2)\n async with AsyncSessionLocal() as db:\n cls = (await db.execute(\n select(EmailClassification).where(EmailClassification.email_id == uuid.UUID(email_id))\n )).scalar_one_or_none()\n if cls:\n print(f\" ✓ worker 已处理 (耗时 ~{(i+1)*2}s)\")\n break\n else:\n print(\" ✗ 超时:worker 未在 60s 内处理。检查 worker 日志。\")\n return\n\n # print the full result the worker persisted\n async with AsyncSessionLocal() as db:\n cls = (await db.execute(\n select(EmailClassification).where(EmailClassification.email_id == uuid.UUID(email_id))\n )).scalar_one()\n reply = (await db.execute(\n select(EmailReply).where(EmailReply.email_id == uuid.UUID(email_id))\n )).scalar_one_or_none()\n ticket = (await db.execute(\n select(Ticket).where(Ticket.email_id == uuid.UUID(email_id))\n )).scalar_one_or_none()\n audits = (await db.execute(\n select(AuditLog).where(AuditLog.email_id == uuid.UUID(email_id)).order_by(AuditLog.created_at)\n )).scalars().all()\n\n print(\"\\n\" + \"═\" * 72)\n print(\"📊 worker 写回 Postgres 的结果\")\n print(\"═\" * 72)\n print(f\"🧠 分类: {cls.email_type} · 语言={cls.language} · 紧急度={cls.urgency}\"\n f\" · 置信度={cls.confidence} · 模型={cls.llm_model}\"\n f\" · tokens={cls.prompt_tokens}+{cls.completion_tokens}\")\n\n print(\"\\n🔗 审计链路 (audit_logs):\")\n for a in audits:\n mark = \"✓\" if a.status == \"success\" else \"✗\"\n print(f\" {mark} {STAGE_CN.get(a.stage, a.stage):8s} {a.detail or ''}\")\n\n if reply:\n print(f\"\\n✍️ 生成回复 (status={reply.status}, strategy={reply.send_strategy}, model={reply.llm_model}):\")\n print(\" \" + (reply.draft_content or \"\").replace(\"\\n\", \"\\n \"))\n print(f\" 引用知识库 chunk: {len(reply.rag_chunk_ids or [])} 条\")\n if ticket:\n print(f\"\\n🎫 工单: {ticket.title} · 原因={ticket.reason} · 优先级={ticket.priority} · 状态={ticket.status}\")\n\n print(\"\\n\" + \"═\" * 72)\n print(\"✅ 完整生产路径验证通过:注入 → ARQ → worker → orchestrator → Postgres\")\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n"}, "afbc3bbfb4cb8f461babdea9e1e00fdc458e9a3e": {"t": 1785766348.66793, "n": 7419, "h": "8c8694e8846bb692", "c": "\"\"\"\nEnd-to-end production-path verification.\n\nProves the REAL runtime path — not a script reimplementation:\n inject inbound email → enqueue on ARQ/Redis → the worker container's\n process_email task → orchestrator → writes classification/reply/ticket/\n audit rows back to Postgres.\n\nSteps:\n 1. Configure the demo tenant's LLM = DeepSeek (encrypted).\n 2. Widen the FAQ KB group's email_types and RE-EMBED its chunks with the\n local embedder (seed stored zero-vector placeholders that can't match).\n 3. Force sales reply-types to draft_review so no fake SMTP send is attempted.\n 4. Inject a fresh inbound email on the sales account.\n 5. enqueue_process_email(...) → real worker picks it up.\n 6. Poll Postgres until the worker writes the classification, then print the\n full audit trail + generated reply / ticket.\n\nRun in a one-off container on the compose network (worker must run latest image):\n DEEPSEEK_KEY=sk-... python scripts/verify_e2e.py\n\"\"\"\nimport asyncio\nimport os\nimport sys\nimport uuid\nfrom datetime import datetime, timezone\n\nfrom sqlalchemy import select\n\nfrom app.core.security import encrypt_value\nfrom app.db.base import AsyncSessionLocal\nfrom app.db.models import (\n AuditLog, Email, EmailAccount, EmailClassification, EmailReply,\n EmailThread, EmailTypeStrategy, KBChunk, KBGroup, Ticket, Tenant,\n)\nfrom app.services.llm.local_embed import LocalHashEmbedProvider\n\nDEMO_DOMAIN = \"demo.emailai.local\"\nSALES_TYPES = [\"customer_inquiry\", \"quote_request\", \"material_request\", \"order_confirm\", \"complaint\", \"other\"]\n\nSTAGE_CN = {\n \"sensitive_blocked\": \"敏感词拦截\", \"classified\": \"分类完成\", \"rag_retrieved\": \"知识库检索\",\n \"reply_generated\": \"生成回复\", \"draft_created\": \"草稿待审\", \"sent\": \"已发送\",\n \"human_routed\": \"转人工\", \"skipped\": \"跳过\",\n}\n\n\nasync def prepare(db, key: str) -> tuple[uuid.UUID, uuid.UUID]:\n embed = LocalHashEmbedProvider()\n\n tenant = (await db.execute(select(Tenant).where(Tenant.domain == DEMO_DOMAIN))).scalar_one()\n\n # 1. LLM = DeepSeek (chat); embeddings fall back to local automatically\n s = dict(tenant.settings or {})\n s[\"llm_config\"] = {\"provider\": \"deepseek\", \"api_key_enc\": encrypt_value(key), \"model\": \"deepseek-chat\"}\n tenant.settings = s\n\n # 2. widen FAQ group types + re-embed all chunks with the real local embedder\n groups = (await db.execute(select(KBGroup).where(KBGroup.tenant_id == tenant.id))).scalars().all()\n for g in groups:\n if g.category == \"faq\" or \"产品\" in g.name:\n g.email_types = SALES_TYPES\n if \"sales\" not in (g.positioning or []):\n g.positioning = list(set((g.positioning or []) + [\"sales\"]))\n\n chunks = (await db.execute(select(KBChunk).where(KBChunk.tenant_id == tenant.id))).scalars().all()\n for c in chunks:\n c.embedding = (await embed.embed([c.content])).embeddings[0]\n print(f\" ✓ 重算 {len(chunks)} 条知识库 chunk 的本地向量\")\n\n # 3. force sales reply-types to draft_review (avoid fake-SMTP auto_send)\n strategies = (await db.execute(\n select(EmailTypeStrategy).where(EmailTypeStrategy.tenant_id == tenant.id)\n )).scalars().all()\n by_type = {st.email_type: st for st in strategies}\n for t in (\"customer_inquiry\", \"quote_request\", \"material_request\"):\n if t in by_type:\n by_type[t].send_strategy = \"draft_review\"\n\n account = (await db.execute(\n select(EmailAccount).where(EmailAccount.tenant_id == tenant.id, EmailAccount.positioning == \"sales\")\n )).scalar_one()\n\n await db.commit()\n return tenant.id, account.id\n\n\nasync def inject(db, tenant_id, account_id) -> str:\n account = (await db.execute(select(EmailAccount).where(EmailAccount.id == account_id))).scalar_one()\n thread = EmailThread(id=uuid.uuid4(), tenant_id=tenant_id, account_id=account_id,\n subject=\"产品规格与交货期咨询\", status=\"open\")\n db.add(thread)\n await db.flush()\n\n email = Email(\n id=uuid.uuid4(), tenant_id=tenant_id, account_id=account_id, thread_id=thread.id,\n message_id=f\"<e2e-{uuid.uuid4()}@buyer.test>\", direction=\"inbound\",\n from_addr=\"wang@acme-trading.com\", from_name=\"采购王经理\",\n to_addrs=[account.email_address], cc_addrs=[],\n subject=\"产品规格与交货期咨询\",\n body_text=\"你好,我们准备下单,请问贵公司产品有哪些规格?标准版和专业版有什么区别?\"\n \"另外交货期大概多久?希望尽快回复,谢谢。\",\n received_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),\n )\n db.add(email)\n await db.commit()\n return str(email.id)\n\n\nasync def main():\n key = os.environ.get(\"DEEPSEEK_KEY\")\n if not key:\n print(\"ERROR: set DEEPSEEK_KEY\", file=sys.stderr)\n sys.exit(1)\n\n print(\"\\n🔧 准备阶段\")\n async with AsyncSessionLocal() as db:\n tenant_id, account_id = await prepare(db, key)\n print(\" ✓ 配置 tenant LLM = DeepSeek,销售类型 → 草稿待审\")\n\n async with AsyncSessionLocal() as db:\n email_id = await inject(db, tenant_id, account_id)\n print(f\" ✓ 注入 inbound 邮件 email_id={email_id}\")\n\n # enqueue onto the REAL ARQ queue — the worker container consumes it\n from app.worker.tasks import enqueue_process_email\n await enqueue_process_email(email_id)\n print(\"\\n📨 已投递到 ARQ 队列,等待 worker 容器处理...\")\n\n # poll until worker writes the classification\n cls = None\n for i in range(30):\n await asyncio.sleep(2)\n async with AsyncSessionLocal() as db:\n cls = (await db.execute(\n select(EmailClassification).where(EmailClassification.email_id == uuid.UUID(email_id))\n )).scalar_one_or_none()\n if cls:\n print(f\" ✓ worker 已处理 (耗时 ~{(i+1)*2}s)\")\n break\n else:\n print(\" ✗ 超时:worker 未在 60s 内处理。检查 worker 日志。\")\n return\n\n # print the full result the worker persisted\n async with AsyncSessionLocal() as db:\n cls = (await db.execute(\n select(EmailClassification).where(EmailClassification.email_id == uuid.UUID(email_id))\n )).scalar_one()\n reply = (await db.execute(\n select(EmailReply).where(EmailReply.email_id == uuid.UUID(email_id))\n )).scalar_one_or_none()\n ticket = (await db.execute(\n select(Ticket).where(Ticket.email_id == uuid.UUID(email_id))\n )).scalar_one_or_none()\n audits = (await db.execute(\n select(AuditLog).where(AuditLog.email_id == uuid.UUID(email_id)).order_by(AuditLog.created_at)\n )).scalars().all()\n\n print(\"\\n\" + \"═\" * 72)\n print(\"📊 worker 写回 Postgres 的结果\")\n print(\"═\" * 72)\n print(f\"🧠 分类: {cls.email_type} · 语言={cls.language} · 紧急度={cls.urgency}\"\n f\" · 置信度={cls.confidence} · 模型={cls.llm_model}\"\n f\" · tokens={cls.prompt_tokens}+{cls.completion_tokens}\")\n\n print(\"\\n🔗 审计链路 (audit_logs):\")\n for a in audits:\n mark = \"✓\" if a.status == \"success\" else \"✗\"\n print(f\" {mark} {STAGE_CN.get(a.stage, a.stage):8s} {a.detail or ''}\")\n\n if reply:\n print(f\"\\n✍️ 生成回复 (status={reply.status}, strategy={reply.send_strategy}, model={reply.llm_model}):\")\n print(\" \" + (reply.draft_content or \"\").replace(\"\\n\", \"\\n \"))\n print(f\" 引用知识库 chunk: {len(reply.rag_chunk_ids or [])} 条\")\n if ticket:\n print(f\"\\n🎫 工单: {ticket.title} · 原因={ticket.reason} · 优先级={ticket.priority} · 状态={ticket.status}\")\n\n print(\"\\n\" + \"═\" * 72)\n print(\"✅ 完整生产路径验证通过:注入 → ARQ → worker → orchestrator → Postgres\")\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n"}}}}