import json from types import SimpleNamespace from app.config.ai_prompts import PROMPT_SPACE_AI_INTERACTION_OPTION_PLANNER from app.schemas.customer_answer import CustomerEvidencePacket from app.schemas.rag import RAGAskRequest from app.services.customer_answer_presentation_service import ( CustomerAnswerPresentationService, _render_state_shape_answer, _standardize_canvas_direct_answer, ) FORBIDDEN_USER_TERMS = ( "System Snapshot", "User Snapshot", "Session State", "Project database", "Knowledge Graph", "PROMPT_", "AdaptiveRAG", "SemanticDecision", "backend/", "frontend/", "tenant_id", "prompt_key", "trace_id", ) def make_service() -> CustomerAnswerPresentationService: return CustomerAnswerPresentationService(object(), tenant_id=1) def assert_customer_safe(text: str) -> None: assert str(text or "").strip() for term in FORBIDDEN_USER_TERMS: assert term not in text def test_space_ai_interaction_option_planner_prompt_formats_with_json_schema() -> None: prompt = PROMPT_SPACE_AI_INTERACTION_OPTION_PLANNER.format( customer_evidence_packet=json.dumps({"answer_type": "strategic_judgement"}, ensure_ascii=False), question="我想做个AI混剪工具", answer="先收敛成一个新创意版本。", fallback_interaction_request=json.dumps( { "title": "还要你确认的点", "questions": [{"id": "target_user", "options": []}], }, ensure_ascii=False, ), ) assert '"title": "还要你确认的点"' in prompt assert '"id": "target_user | pain_point | mvp_loop | validation_signal | ..."' in prompt assert "我想做个AI混剪工具" in prompt def test_customer_evidence_packet_accepts_runtime_answer_shapes_and_sanitizes_terms() -> None: packet = CustomerEvidencePacket.model_validate( { "question": "我想做个 AI 采购监督系统", "answer_type": "strategic_judgement", "facts": [ { "type": "runtime_evidence_finding", "display_label": "Project database 里的外部证据", "display_summary": "System Snapshot 显示监管压力真实存在", "business_values": {"含义": "监管压力真实存在", "tenant_id": "raw"}, "source_display_labels": ["Project database"], } ], "source_summary": ["Project database"], } ).to_customer_prompt_json() dumped = json.dumps(packet, ensure_ascii=False) assert packet["answer_type"] == "strategic_judgement" assert_customer_safe(dumped) assert "监管压力真实存在" in dumped def test_renderer_uses_turn_state_answer_shape_not_legacy_route_contract() -> None: service = make_service() packet = service.build_customer_evidence_packet( request=RAGAskRequest( question="我想做个 AI 采购监督系统,你觉得怎么样", surface_context={ "workbench": { "recentProjects": [ { "id": 141, "title": "童析-学龄前儿童行为特质AI评估助手", "status": "active", } ] } }, ), current_user=SimpleNamespace(id=7, tenant_id=1), payload={ "pmflow_ai_route_plan": { "goal_kind": "project_inventory", "answer_contract": "surface_fact", "source_strategy": "internal_only", }, "pmflow_ai_turn_state": { "answer_state": {"answer_shape": "strategic_judgement"}, "object_state": {"primary_object": {"object_type": "new_idea", "binding_strength": "none"}}, }, "evidence_findings": [ { "fact": "公开政策显示采购监督压力真实存在。", "implication": "机会不在报告生成,而在风险监督闭环。", "source": "https://example.com/policy", "relevance_to_goal": "validates_market_window", "confidence": "high", } ], "sources": [{"source": "external", "display_label": "政策来源", "target_url": "https://example.com/policy"}], }, ) dumped = json.dumps(packet, ensure_ascii=False) assert packet["answer_type"] == "strategic_judgement" assert "童析" not in dumped assert "机会不在报告生成" in dumped def test_renderer_demotes_legacy_packet_answer_type_to_runtime_general_shape() -> None: service = make_service() packet = service.build_customer_evidence_packet( request=RAGAskRequest(question="这个项目的 MVP 还差多少"), current_user=SimpleNamespace(id=7, tenant_id=1), payload={ "customer_evidence_packet": {"answer_type": "project_mvp_readiness"}, "evidence_findings": [ { "fact": "当前项目缺少正式任务和验收标准。", "source": "当前项目执行态证据", "relevance_to_goal": "判断 MVP readiness", "confidence": "high", } ], }, ) canvas = service.build_answer_canvas(packet=packet, answer="当前项目缺少正式任务和验收标准。") assert packet["answer_type"] == "general_answer" assert canvas["headline"] == "回答" def test_process_packet_does_not_fabricate_renderer_steps() -> None: service = make_service() process = service.build_answer_process_packet( packet={ "answer_type": "strategic_judgement", "facts": [ { "type": "runtime_evidence_finding", "display_label": "外部公开来源", "display_summary": "机会不在浅工具,而在闭环。", "confidence": "high", } ], }, verification={"ok": True}, ) assert process["answer_type"] == "strategic_judgement" assert process["steps"] == [] def test_process_packet_preserves_explicit_runtime_steps_only() -> None: service = make_service() process = service.build_answer_process_packet( packet={ "answer_type": "execution_guidance", "process_steps": [ { "key": "inspect_project_status", "label": "读取项目执行态证据", "status": "completed", "duration_ms": 120, } ], }, verification={"ok": True}, ) assert process["steps"][0]["key"] == "inspect_project_status" assert process["steps"][0]["label"] == "读取项目执行态证据" def test_customer_evidence_packet_does_not_invent_default_source_summary() -> None: service = make_service() packet = service.build_customer_evidence_packet( request=RAGAskRequest(question="现在有什么项目"), current_user=SimpleNamespace(id=7, tenant_id=1), payload={ "grounding_status": "partially_grounded", "evidence_findings": [ { "fact": "当前可见项目 1 个", "implication": "可以回答项目数量", "source": "", } ], }, ) assert packet["source_summary"] == [] def test_answer_canvas_headline_and_evidence_are_shape_based() -> None: service = make_service() answer = "判断:值得继续看,但要做成监督闭环,不是报告生成。" canvas = service.build_answer_canvas( packet={ "answer_type": "strategic_judgement", "facts": [ { "type": "runtime_evidence_finding", "display_label": "外部公开来源", "display_summary": "监管和内控压力是真实存在的。", "confidence": "high", "source_display_labels": ["外部公开来源"], } ], "limitations": ["还缺目标客户预算证据"], }, answer=answer, ) assert canvas["headline"] == "方向判断" assert canvas["direct_answer"] == "值得继续看,但要做成监督闭环,不是报告生成。" assert canvas["evidence_blocks"][0]["summary"] == "监管和内控压力是真实存在的。" assert canvas["limitations"] == ["还缺目标客户预算证据"] def test_packet_az_canvas_direct_answer_does_not_generate_fixed_evidence_sections() -> None: answer = ( "这个方向可以继续看,但要先验证采购团队是否愿意为监督闭环付费。" "已有监管压力能支持问题存在。" "还缺预算和真实流程证据。" "第一版先做风险提示和留痕。" ) direct = _standardize_canvas_direct_answer(answer) assert "**结论**" not in direct assert "**判断依据**" not in direct assert "这个方向可以继续看" in direct assert "第一版先做风险提示" in direct def test_packet_az_state_shape_answer_does_not_prefix_by_answer_type() -> None: answer = _render_state_shape_answer( { "answer_type": "execution_guidance", "facts": [ {"display_summary": "先整理加密评审材料"}, {"display_summary": "直接发布还缺测试质量证据"}, ], } ) assert answer == "先整理加密评审材料;直接发布还缺测试质量证据。" assert not answer.startswith("可执行建议:") def test_packet_az_canvas_sanitizer_strips_generated_section_headings() -> None: direct = _standardize_canvas_direct_answer( "**结论**\n可以继续看。\n\n**判断依据**\n- 已有明确需求。\n- 还缺预算证据。" ) assert "**结论**" not in direct assert "**判断依据**" not in direct assert "可以继续看" in direct assert "已有明确需求" in direct