PMFlow/backend/alembic/versions/20260516_refresh_agent_goal...

190 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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},
)