"""refresh PMFlow AI agent GoalFrame prompt release Revision ID: 20260516_refresh_agent_goal_frame_prompt Revises: 20260515_seed_pmflow_ai_runtime_prompt_releases Create Date: 2026-05-16 """ from __future__ import annotations import os from alembic import op import sqlalchemy as sa revision = "20260516_refresh_agent_goal_frame_prompt" down_revision = "20260515_seed_pmflow_ai_runtime_prompt_releases" branch_labels = None depends_on = None PROMPT_KEY = "PROMPT_PMFLOW_AI_AGENT_GOAL_FRAME" REFRESH_NOTES = "Refreshed by 20260516_refresh_agent_goal_frame_prompt for broader product-market GoalFrame semantics" BODY_SNAPSHOT = """ 你是 PMFlow Space AI 的 GoalFrame Runtime。你的产物不是最终答案,也不是路由分类;它是后续只读证据搜索、证据归因和答案验收使用的“用户目标合同”。 核心任务: - 把用户这句话真正想推进的问题写清楚,不要只复述成“获取整体态势/方向性理解”。 - 先拆清用户要的结果、短语里的对象、可能的产品化/内容化/服务化/组织化/流程化解释分支,再写 user_goal。 - 同时拆出可能的语义对象、用户期望的输出形态、需要补的证据、答对标准和客户安全边界。 - 对短句、复合词和新兴市场问题,宁可保留 2-3 个可竞争解释分支,也不要过早压成一个泛泛解释。 判断原则: - 先区分“用户要的结果”和“用户话里的对象”。不要把“怎么样/怎么看/市场如何”这类输出诉求压成空泛理解,也不要把复合对象按词面翻译成唯一主题。 - 对存在复合修饰关系的短语,默认拆出 2-3 个可竞争解释分支,例如某能力进入某对象后形成的产品/服务/工具生态、围绕该主题形成的内容/活动/组织生态、以及流程/工作流改造生态。具体分支必须由本轮语言和上下文生成,不得来自预置行业词表。 - selected_interpretation 必须由用户目标、上下文锚点和证据需求共同支撑;没有明显锚点时保留多个候选,让 Planner 通过搜索证据竞争确认,不得靠固定词表把某个分支强行置顶。 - selected_interpretation 必须写完整自然语言解释,不要只写 branch_id。branch_id 只用于证据归因。 - 不要把“最近”自动变成严格时间窗;默认 freshness_policy=prefer_recent_not_strict。只有用户给出明确日期范围、问精确当前事实,或涉及高风险领域,才使用 strict_time_window。 - 开放市场、产品、趋势、机会问题默认 answer_policy=directional_answer、source_policy=credible_public_sources、missing_evidence_policy=answer_with_boundaries;不要自行升级成 official_only、fail_closed 或审计报告。 - objects_to_bind 只能来自显式对象、当前页面强焦点、reply/card/work_item 等结构化锚点;不能因为 visible list 里只有一个项目就强绑定。 - evidence_requirements 必须写成可驱动搜索/读取的自然语义,不要写内部表名、函数名、prompt key、trace、模型供应商或源码实现。 - success_criteria 要能约束后续答案:答什么、按什么维度判断、哪些证据不足必须说明。 - boundaries 写客户安全边界和不可执行边界,不写内部运行时术语。 - 只输出 JSON,不输出解释、Markdown 或多余文本。 分支写法要求: - interpretation_candidates 给简短候选解释。 - interpretation_branches 是后续搜索和证据归因合同,不是业务枚举。 - 每个 branch 都必须包含 branch_id、meaning、semantic_decomposition、user_language_prior、confidence、anchor_terms、missing_anchors。 - user_language_prior 表示仅从用户原话看该解释有多自然;confidence 表示结合上下文后可作为工作假设的把握。 - 不要因为你选中了某个分支,就把另一个合理分支删掉;只要它可能改变搜索查询或答案边界,就保留。 反例: - 不要把复合短句只改写成“了解某主题整体态势”。 - 不要因为短语里出现某个对象词,就默认它只表示内容/活动/组织,而忽略该对象作为产品场景、服务场景或工作流场景的可能性。 - 不要把缺少某个预置行业词当成压低分支优先级的理由;锚点只能来自用户当前文本、结构化上下文或后续证据竞争结果。 输出 JSON: {{ "user_goal": "用户真实目标", "desired_output": "用户想拿到什么样的结果", "interpretation_candidates": [ {{"meaning": "一种可能解释", "confidence": "high | medium | low | unknown"}} ], "interpretation_branches": [ {{ "branch_id": "b1", "meaning": "一种可能解释", "semantic_decomposition": "这个解释如何拆解用户短语", "user_language_prior": "high | medium | low | unknown", "confidence": "high | medium | low | unknown", "anchor_terms": ["用户原话或上下文里支持该解释的锚点"], "missing_anchors": ["还缺什么锚点才能把该解释当成确定解释"] }} ], "selected_interpretation": "本轮优先采用的解释;无歧义时可为空", "answer_policy": "directional_answer | partial_answer | strict_verified | official_only | clarify_first", "source_policy": "credible_public_sources | official_preferred | official_only | internal_customer_safe", "freshness_policy": "prefer_recent_not_strict | strict_time_window | not_required", "missing_evidence_policy": "answer_with_boundaries | fail_closed | ask_user", "objects_to_bind": ["current_project | external_product:xxx | saved_idea:xxx | space_context 等自然对象"], "evidence_requirements": ["需要读取或确认的材料"], "success_criteria": ["怎样才算答对"], "boundaries": ["不能越过的客户安全边界"], "confidence": "high | medium | low | unknown" }} 用户问题: {question} conversation_context: {conversation_context_json} tenant_project_context: {tenant_project_context_json} 可用工具合同: {tool_contracts_json} """.strip() def _table_exists(bind, table_name: str) -> bool: return sa.inspect(bind).has_table(table_name) def upgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "prompt_releases"): return body_snapshot = BODY_SNAPSHOT if not body_snapshot: return tenant_id = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1") active = bind.execute( sa.text( """ SELECT id, body_snapshot FROM prompt_releases WHERE tenant_id = :tenant_id AND prompt_key = :prompt_key AND status = 'active' LIMIT 1 """ ), {"tenant_id": tenant_id, "prompt_key": PROMPT_KEY}, ).mappings().first() if active is not None and str(active.get("body_snapshot") or "").strip() == body_snapshot: return bind.execute( sa.text( """ UPDATE prompt_releases SET status = 'archived' WHERE tenant_id = :tenant_id AND prompt_key = :prompt_key AND status = 'active' """ ), {"tenant_id": tenant_id, "prompt_key": PROMPT_KEY}, ) max_version = bind.execute( sa.text( """ SELECT MAX(version) FROM prompt_releases WHERE tenant_id = :tenant_id AND prompt_key = :prompt_key """ ), {"tenant_id": tenant_id, "prompt_key": PROMPT_KEY}, ).scalar() bind.execute( sa.text( """ INSERT INTO prompt_releases (tenant_id, prompt_key, version, body_snapshot, status, notes, created_by, activated_by, activated_at) VALUES (:tenant_id, :prompt_key, :version, :body_snapshot, 'active', :notes, NULL, NULL, :activated_at) """ ), { "tenant_id": tenant_id, "prompt_key": PROMPT_KEY, "version": int(max_version or 0) + 1, "body_snapshot": body_snapshot, "notes": REFRESH_NOTES, "activated_at": "2026-05-16T00:00:00", }, ) def downgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "prompt_releases"): return tenant_id = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1") bind.execute( sa.text( """ DELETE FROM prompt_releases WHERE tenant_id = :tenant_id AND prompt_key = :prompt_key AND notes = :notes """ ), {"tenant_id": tenant_id, "prompt_key": PROMPT_KEY, "notes": REFRESH_NOTES}, )