190 lines
8.6 KiB
Python
190 lines
8.6 KiB
Python
"""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},
|
||
)
|