Prechádzať zdrojové kódy

feat(langgraph): 方案 section 重生成返回 {content, tasks} 结构化

iwt 3 týždňov pred
rodič
commit
4f25d652a2

+ 39 - 3
cfc-langgraph/app/api/adapter.py

@@ -665,8 +665,25 @@ PLAN_SYSTEM_PROMPT = """你是一个专业的家庭健康方案规划师。根
 
 REGENERATE_SECTION_SYSTEM_PROMPT = """你是一个家庭健康方案规划师。根据用户反馈重新生成指定部分内容。
 
-## 输出格式
-只输出新的 content 字段值(Markdown 格式字符串),不要输出 JSON 结构。
+## 输出格式(必须输出合法 JSON,不要有其他内容)
+{
+  "content": "重新生成的 Markdown 内容",
+  "tasks": [
+    {
+      "action_type": "buy|read|exercise|checkin|diet|activity",
+      "title": "可执行任务标题",
+      "dimension": "body|mind|wisdom|action|wealth",
+      "frequency": "once|daily",
+      "notes": "补充说明"
+    }
+  ]
+}
+
+## tasks 约定
+- action_type 取值:buy/read/exercise/checkin/diet/activity
+- dimension 取值:body/mind/wisdom/action/wealth
+- frequency:once=一次性;daily=每日重复
+- 无行动项时 tasks 输出 []
 
 ## 原则
 - 保持与原格式一致
@@ -925,7 +942,26 @@ async def health_plan_regenerate(req: HealthPlanRegenerateRequest):
 
     try:
         response = await llm.ainvoke([SystemMessage(content=prompt)])
-        return {"success": True, "content": response.content}
+        answer = response.content
+        start = answer.find("{")
+        end = answer.rfind("}") + 1
+        content = answer
+        tasks = []
+        if start >= 0 and end > start:
+            try:
+                parsed = json.loads(answer[start:end])
+                content = parsed.get("content") or answer
+                raw_tasks = parsed.get("tasks") or []
+                # 用 PlanTask 校验,非法条目丢弃
+                for t in raw_tasks:
+                    try:
+                        pt = PlanTask.model_validate(t)
+                        tasks.append(pt.model_dump())
+                    except Exception:
+                        continue
+            except Exception as e:
+                logger.warning("解析重生成 section JSON 失败: %s", e)
+        return {"success": True, "content": content, "tasks": tasks}
     except Exception as e:
         logger.error("重新生成 section 失败: %s", e)
         return {"success": False, "error": str(e)}

+ 15 - 0
cfc-langgraph/tests/test_health_plan.py

@@ -66,3 +66,18 @@ def test_parse_plan_response_no_json():
 def test_fallback_skips_when_tasks_exist():
     parsed = {"sections": [{"key": "diet", "title": "t", "content": "1. 少油少盐", "tasks": [{"action_type": "diet", "title": "已有", "dimension": "body"}]}]}
     assert _fallback_extract_tasks(parsed) is None
+
+
+def test_regenerate_tasks_validation_filter():
+    raw_tasks = [
+        {"action_type": "diet", "title": "少油少盐", "dimension": "body", "frequency": "daily"},
+        {"action_type": "cook", "title": "非法", "dimension": "body"},  # 非法 action,应被丢弃
+    ]
+    valid = []
+    for t in raw_tasks:
+        try:
+            valid.append(PlanTask.model_validate(t).model_dump())
+        except Exception:
+            continue
+    assert len(valid) == 1
+    assert valid[0]["action_type"] == "diet"