mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
feat(pipeline): 策略生成按矩阵分组收窄 brief 输入
新增 strategy_matrix_group / strategy_matrix_group_index 请求参数;收窄后价盘与集中度按分组内 SKU 重算;摘要 GET 与策略 POST 响应附带 matrix_groups。 Made-with: Cursor
This commit is contained in:
parent
911ad203db
commit
6143ad6180
304
backend/pipeline/reporting/brief_strategy_scope.py
Normal file
304
backend/pipeline/reporting/brief_strategy_scope.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
"""按矩阵分组(细类)收窄 competitor brief,使策略生成输入仅含所选分组数据。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from collections import Counter
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pipeline.competitor_report.csv_io import _collect_prices
|
||||||
|
from pipeline.competitor_report.price_stats import _price_stats_extended
|
||||||
|
|
||||||
|
|
||||||
|
def list_matrix_groups_for_api(brief: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
"""供前端下拉:矩阵分组名称与索引(与 ``matrix_by_group`` 顺序一致)。"""
|
||||||
|
mg = brief.get("matrix_by_group")
|
||||||
|
if not isinstance(mg, list):
|
||||||
|
return []
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for i, g in enumerate(mg):
|
||||||
|
if not isinstance(g, dict):
|
||||||
|
continue
|
||||||
|
name = (g.get("group") or "").strip() or "—"
|
||||||
|
skus = g.get("skus") if isinstance(g.get("skus"), list) else []
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"index": i,
|
||||||
|
"group": name,
|
||||||
|
"sku_count": int(g.get("sku_count") or len(skus)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_strategy_matrix_group_index(
|
||||||
|
brief: dict[str, Any],
|
||||||
|
*,
|
||||||
|
matrix_group_index: int | None = None,
|
||||||
|
matrix_group_label: str | None = None,
|
||||||
|
) -> tuple[int | None, str | None]:
|
||||||
|
"""
|
||||||
|
解析请求中的矩阵分组。
|
||||||
|
|
||||||
|
返回 ``(index, error)``:
|
||||||
|
- ``index is None`` 且 ``error is None``:未指定收窄(使用完整 brief);
|
||||||
|
- ``index`` 为 ``int``:有效分组下标;
|
||||||
|
- ``error`` 非空:参数与 brief 不一致。
|
||||||
|
"""
|
||||||
|
mg = brief.get("matrix_by_group")
|
||||||
|
if not isinstance(mg, list) or not mg:
|
||||||
|
if matrix_group_index is not None or (
|
||||||
|
matrix_group_label and str(matrix_group_label).strip()
|
||||||
|
):
|
||||||
|
return None, "当前任务 brief 中无 matrix_by_group,无法按细类收窄"
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
label = (matrix_group_label or "").strip()
|
||||||
|
has_idx = matrix_group_index is not None
|
||||||
|
has_lbl = bool(label)
|
||||||
|
|
||||||
|
if not has_idx and not has_lbl:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if has_idx:
|
||||||
|
idx = int(matrix_group_index)
|
||||||
|
if idx < 0 or idx >= len(mg):
|
||||||
|
return None, f"strategy_matrix_group_index 须在 0~{len(mg) - 1} 之间"
|
||||||
|
|
||||||
|
if has_lbl and not has_idx:
|
||||||
|
for i, g in enumerate(mg):
|
||||||
|
if not isinstance(g, dict):
|
||||||
|
continue
|
||||||
|
if (g.get("group") or "").strip() == label:
|
||||||
|
return i, None
|
||||||
|
return None, f"未找到矩阵分组「{label}」"
|
||||||
|
|
||||||
|
assert has_idx
|
||||||
|
idx = int(matrix_group_index)
|
||||||
|
g0 = mg[idx]
|
||||||
|
gname = (g0.get("group") or "").strip() if isinstance(g0, dict) else ""
|
||||||
|
if has_lbl and gname != label:
|
||||||
|
return None, (
|
||||||
|
f"strategy_matrix_group_index={idx} 对应分组「{gname}」,"
|
||||||
|
f"与 strategy_matrix_group「{label}」不一致"
|
||||||
|
)
|
||||||
|
return idx, None
|
||||||
|
|
||||||
|
|
||||||
|
def _sku_rows_for_prices(skus: list[dict[str, Any]]) -> list[dict[str, str]]:
|
||||||
|
out: list[dict[str, str]] = []
|
||||||
|
for s in skus:
|
||||||
|
if not isinstance(s, dict):
|
||||||
|
continue
|
||||||
|
out.append({k: str(v) if v is not None else "" for k, v in s.items()})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _mix_top_rows(
|
||||||
|
skus: list[dict[str, Any]], key: str, *, top_n: int = 24
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
c: Counter[str] = Counter()
|
||||||
|
for s in skus:
|
||||||
|
if not isinstance(s, dict):
|
||||||
|
continue
|
||||||
|
v = (s.get(key) or "").strip() or "(未标注)"
|
||||||
|
c[v] += 1
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for lab, cnt in c.most_common(top_n):
|
||||||
|
rows.append({"label": lab, "count": cnt})
|
||||||
|
remainder = sum(cnt for _, cnt in c.most_common()[top_n:])
|
||||||
|
if remainder > 0:
|
||||||
|
rows.append({"label": "(其余)", "count": remainder})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _category_mix_from_skus(skus: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
c: Counter[str] = Counter()
|
||||||
|
for s in skus:
|
||||||
|
if not isinstance(s, dict):
|
||||||
|
continue
|
||||||
|
cat = (s.get("category") or "").strip()
|
||||||
|
if not cat:
|
||||||
|
continue
|
||||||
|
parts = [p.strip() for p in cat.split(">") if p.strip()]
|
||||||
|
leaf = parts[-1] if parts else cat
|
||||||
|
c[leaf] += 1
|
||||||
|
return [{"label": k, "count": v} for k, v in c.most_common(24)]
|
||||||
|
|
||||||
|
|
||||||
|
def _concentration_brand_shop_from_skus(skus: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
"""用矩阵内 SKU 粗算集中度(与全站列表口径不同,仅供分组内对照)。"""
|
||||||
|
brands = [
|
||||||
|
(s.get("brand") or "").strip()
|
||||||
|
for s in skus
|
||||||
|
if isinstance(s, dict) and (s.get("brand") or "").strip()
|
||||||
|
]
|
||||||
|
shops = [
|
||||||
|
(s.get("shop") or "").strip()
|
||||||
|
for s in skus
|
||||||
|
if isinstance(s, dict) and (s.get("shop") or "").strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
def _block(labels: list[str]) -> dict[str, Any]:
|
||||||
|
if not labels:
|
||||||
|
return {
|
||||||
|
"first_share": 0.0,
|
||||||
|
"top_three_combined_share": 0.0,
|
||||||
|
"top_label": "—",
|
||||||
|
"top_share_pct": "0%",
|
||||||
|
}
|
||||||
|
ct = Counter(labels)
|
||||||
|
n = len(labels)
|
||||||
|
top_lab, top_n = ct.most_common(1)[0]
|
||||||
|
top3 = sum(x for _, x in ct.most_common(3))
|
||||||
|
return {
|
||||||
|
"first_share": top_n / n,
|
||||||
|
"top_three_combined_share": top3 / n,
|
||||||
|
"top_label": top_lab,
|
||||||
|
"top_share_pct": f"{100.0 * top_n / n:.1f}%",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"shops_from_list": _block(shops),
|
||||||
|
"list_brand_field": None,
|
||||||
|
"detail_brand_among_merged": _block(brands),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def filter_brief_for_strategy_matrix_group(
|
||||||
|
brief: dict[str, Any],
|
||||||
|
*,
|
||||||
|
matrix_group_index: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
深拷贝 ``brief``,仅保留 ``matrix_by_group[matrix_group_index]`` 及其对齐的
|
||||||
|
``consumer_feedback_by_matrix_group`` / ``usage_scenarios_by_matrix_group``,
|
||||||
|
并重算与样本相关的价盘、类目与集中度(避免仍混入全关键词列表口径)。
|
||||||
|
"""
|
||||||
|
b = copy.deepcopy(brief)
|
||||||
|
mg = b.get("matrix_by_group")
|
||||||
|
if not isinstance(mg, list) or matrix_group_index < 0 or matrix_group_index >= len(
|
||||||
|
mg
|
||||||
|
):
|
||||||
|
return b
|
||||||
|
|
||||||
|
chosen = mg[matrix_group_index]
|
||||||
|
if not isinstance(chosen, dict):
|
||||||
|
return b
|
||||||
|
|
||||||
|
gname = (chosen.get("group") or "").strip() or "—"
|
||||||
|
skus = chosen.get("skus") if isinstance(chosen.get("skus"), list) else []
|
||||||
|
n_skus = len(skus)
|
||||||
|
|
||||||
|
b["matrix_by_group"] = [
|
||||||
|
{
|
||||||
|
"group": gname,
|
||||||
|
"sku_count": int(chosen.get("sku_count") or n_skus),
|
||||||
|
"skus": skus,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _pick_by_group(
|
||||||
|
items: Any,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if not isinstance(items, list):
|
||||||
|
return []
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for it in items:
|
||||||
|
if not isinstance(it, dict):
|
||||||
|
continue
|
||||||
|
if (it.get("group") or "").strip() == gname:
|
||||||
|
out.append(dict(it))
|
||||||
|
break
|
||||||
|
gi = it.get("matrix_group_index")
|
||||||
|
if gi is not None and int(gi) == matrix_group_index:
|
||||||
|
out.append(dict(it))
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
fb = b.get("consumer_feedback_by_matrix_group")
|
||||||
|
fb_one = _pick_by_group(fb)
|
||||||
|
b["consumer_feedback_by_matrix_group"] = fb_one
|
||||||
|
|
||||||
|
ub = b.get("usage_scenarios_by_matrix_group")
|
||||||
|
b["usage_scenarios_by_matrix_group"] = _pick_by_group(ub)
|
||||||
|
|
||||||
|
if fb_one:
|
||||||
|
f0 = fb_one[0]
|
||||||
|
b["comment_focus_keywords"] = list(f0.get("focus_keyword_hits") or [])
|
||||||
|
scenarios_top = f0.get("scenarios_top") or []
|
||||||
|
b["usage_scenarios"] = list(scenarios_top)
|
||||||
|
denom = int(f0.get("effective_comment_text_units") or 0)
|
||||||
|
if denom <= 0:
|
||||||
|
denom = int(f0.get("comment_rows") or 0)
|
||||||
|
b["usage_scenarios_denominator"] = denom
|
||||||
|
else:
|
||||||
|
b["comment_focus_keywords"] = []
|
||||||
|
b["usage_scenarios"] = []
|
||||||
|
b["usage_scenarios_denominator"] = 0
|
||||||
|
|
||||||
|
rows_for_price = _sku_rows_for_prices(
|
||||||
|
[s for s in skus if isinstance(s, dict)]
|
||||||
|
)
|
||||||
|
prices = _collect_prices(rows_for_price)
|
||||||
|
pst_merged = _price_stats_extended(prices)
|
||||||
|
b["price_stats_merged_sample"] = pst_merged
|
||||||
|
b["price_stats"] = dict(pst_merged) if pst_merged else {}
|
||||||
|
b["price_stats_source"] = "strategy_scope_matrix_group_skus"
|
||||||
|
b["price_stats_list_export"] = {}
|
||||||
|
|
||||||
|
b["category_mix_top"] = _category_mix_from_skus(
|
||||||
|
[s for s in skus if isinstance(s, dict)]
|
||||||
|
)
|
||||||
|
b["list_brand_mix_top"] = _mix_top_rows(
|
||||||
|
[s for s in skus if isinstance(s, dict)], "brand"
|
||||||
|
)
|
||||||
|
b["list_shop_mix_top"] = _mix_top_rows(
|
||||||
|
[s for s in skus if isinstance(s, dict)], "shop"
|
||||||
|
)
|
||||||
|
|
||||||
|
b["concentration"] = _concentration_brand_shop_from_skus(
|
||||||
|
[s for s in skus if isinstance(s, dict)]
|
||||||
|
)
|
||||||
|
|
||||||
|
sc = b.get("scope")
|
||||||
|
if isinstance(sc, dict):
|
||||||
|
sc2 = dict(sc)
|
||||||
|
sc2["merged_sku_count"] = n_skus
|
||||||
|
if fb_one:
|
||||||
|
sc2["comment_flat_rows"] = int(fb_one[0].get("comment_rows") or 0)
|
||||||
|
b["scope"] = sc2
|
||||||
|
|
||||||
|
b["strategy_scope_applied"] = {
|
||||||
|
"matrix_group_index": matrix_group_index,
|
||||||
|
"group": gname,
|
||||||
|
"original_matrix_group_count": len(mg),
|
||||||
|
}
|
||||||
|
|
||||||
|
notes = b.get("notes")
|
||||||
|
extra = (
|
||||||
|
"策略生成已按矩阵分组收窄:下文统计与矩阵仅针对「"
|
||||||
|
+ gname
|
||||||
|
+ "」内 SKU;与全关键词搜索列表、全样本评价总量不同口径。"
|
||||||
|
)
|
||||||
|
if isinstance(notes, list):
|
||||||
|
b["notes"] = [extra] + [n for n in notes if isinstance(n, str)]
|
||||||
|
else:
|
||||||
|
b["notes"] = [extra]
|
||||||
|
|
||||||
|
b["price_promotion_signals"] = []
|
||||||
|
b["strategy_hints"] = []
|
||||||
|
|
||||||
|
b["list_visibility_proxy"] = {
|
||||||
|
"total_rows": n_skus,
|
||||||
|
"unique_skus": n_skus,
|
||||||
|
"_strategy_scope_note": "矩阵所选分组内 SKU 数,非全关键词列表导出口径。",
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"filter_brief_for_strategy_matrix_group",
|
||||||
|
"list_matrix_groups_for_api",
|
||||||
|
"resolve_strategy_matrix_group_index",
|
||||||
|
]
|
||||||
@ -461,3 +461,15 @@ class StrategyDraftRequestSerializer(serializers.Serializer):
|
|||||||
default="rules",
|
default="rules",
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
strategy_matrix_group_index = serializers.IntegerField(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
min_value=0,
|
||||||
|
)
|
||||||
|
strategy_matrix_group = serializers.CharField(
|
||||||
|
required=False,
|
||||||
|
allow_blank=True,
|
||||||
|
default="",
|
||||||
|
max_length=200,
|
||||||
|
trim_whitespace=True,
|
||||||
|
)
|
||||||
|
|||||||
126
backend/pipeline/tests/test_brief_strategy_scope.py
Normal file
126
backend/pipeline/tests/test_brief_strategy_scope.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""brief 按矩阵分组收窄。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from pipeline.reporting.brief_strategy_scope import (
|
||||||
|
filter_brief_for_strategy_matrix_group,
|
||||||
|
list_matrix_groups_for_api,
|
||||||
|
resolve_strategy_matrix_group_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BriefStrategyScopeTests(SimpleTestCase):
|
||||||
|
def _sample_brief(self) -> dict:
|
||||||
|
return {
|
||||||
|
"schema_version": 1,
|
||||||
|
"keyword": "低GI",
|
||||||
|
"matrix_by_group": [
|
||||||
|
{
|
||||||
|
"group": "饮料",
|
||||||
|
"sku_count": 1,
|
||||||
|
"skus": [
|
||||||
|
{
|
||||||
|
"brand": "A",
|
||||||
|
"shop": "S1",
|
||||||
|
"category": "水饮 > 茶",
|
||||||
|
"list_price_show": "10",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "饼干",
|
||||||
|
"sku_count": 2,
|
||||||
|
"skus": [
|
||||||
|
{
|
||||||
|
"brand": "B",
|
||||||
|
"shop": "S2",
|
||||||
|
"category": "休闲食品 > 饼干 > 粗粮饼干",
|
||||||
|
"list_price_show": "20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "B",
|
||||||
|
"shop": "S2",
|
||||||
|
"category": "休闲食品 > 饼干 > 苏打饼干",
|
||||||
|
"list_price_show": "22",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"consumer_feedback_by_matrix_group": [
|
||||||
|
{
|
||||||
|
"group": "饮料",
|
||||||
|
"comment_rows": 5,
|
||||||
|
"effective_comment_text_units": 5,
|
||||||
|
"focus_keyword_hits": [{"word": "甜", "count": 2}],
|
||||||
|
"scenarios_top": [
|
||||||
|
{
|
||||||
|
"scenario": "解渴",
|
||||||
|
"count": 2,
|
||||||
|
"share_of_text_units": 0.4,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "饼干",
|
||||||
|
"comment_rows": 8,
|
||||||
|
"effective_comment_text_units": 8,
|
||||||
|
"focus_keyword_hits": [{"word": "脆", "count": 3}],
|
||||||
|
"scenarios_top": [
|
||||||
|
{
|
||||||
|
"scenario": "早餐",
|
||||||
|
"count": 4,
|
||||||
|
"share_of_text_units": 0.5,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"usage_scenarios_by_matrix_group": [
|
||||||
|
{"group": "饮料", "scenarios": []},
|
||||||
|
{"group": "饼干", "scenarios": [{"scenario": "早餐", "count": 4}]},
|
||||||
|
],
|
||||||
|
"notes": ["原注"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_list_matrix_groups(self) -> None:
|
||||||
|
b = self._sample_brief()
|
||||||
|
m = list_matrix_groups_for_api(b)
|
||||||
|
self.assertEqual(len(m), 2)
|
||||||
|
self.assertEqual(m[1]["group"], "饼干")
|
||||||
|
self.assertEqual(m[1]["index"], 1)
|
||||||
|
|
||||||
|
def test_resolve_by_label(self) -> None:
|
||||||
|
b = self._sample_brief()
|
||||||
|
idx, err = resolve_strategy_matrix_group_index(
|
||||||
|
b, matrix_group_label="饼干"
|
||||||
|
)
|
||||||
|
self.assertIsNone(err)
|
||||||
|
self.assertEqual(idx, 1)
|
||||||
|
|
||||||
|
def test_resolve_by_index(self) -> None:
|
||||||
|
b = self._sample_brief()
|
||||||
|
idx, err = resolve_strategy_matrix_group_index(
|
||||||
|
b, matrix_group_index=1
|
||||||
|
)
|
||||||
|
self.assertIsNone(err)
|
||||||
|
self.assertEqual(idx, 1)
|
||||||
|
|
||||||
|
def test_resolve_mismatch(self) -> None:
|
||||||
|
b = self._sample_brief()
|
||||||
|
idx, err = resolve_strategy_matrix_group_index(
|
||||||
|
b, matrix_group_index=0, matrix_group_label="饼干"
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(err)
|
||||||
|
self.assertIsNone(idx)
|
||||||
|
|
||||||
|
def test_filter_keeps_only_group(self) -> None:
|
||||||
|
b = self._sample_brief()
|
||||||
|
out = filter_brief_for_strategy_matrix_group(b, matrix_group_index=1)
|
||||||
|
self.assertEqual(len(out["matrix_by_group"]), 1)
|
||||||
|
self.assertEqual(out["matrix_by_group"][0]["group"], "饼干")
|
||||||
|
self.assertEqual(len(out["matrix_by_group"][0]["skus"]), 2)
|
||||||
|
self.assertEqual(len(out["consumer_feedback_by_matrix_group"]), 1)
|
||||||
|
self.assertEqual(out["comment_focus_keywords"][0]["word"], "脆")
|
||||||
|
self.assertEqual(out["price_stats_source"], "strategy_scope_matrix_group_skus")
|
||||||
|
self.assertIn("strategy_scope_applied", out)
|
||||||
|
self.assertEqual(out["strategy_scope_applied"]["group"], "饼干")
|
||||||
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
from typing import Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -21,6 +22,11 @@ from ..jd.runner import (
|
|||||||
from ..llm.generate import generate_strategy_draft_markdown_llm
|
from ..llm.generate import generate_strategy_draft_markdown_llm
|
||||||
from ..models import JobStatus, PipelineJob
|
from ..models import JobStatus, PipelineJob
|
||||||
from ..reporting.brief_pack import build_brief_pack_zip_bytes
|
from ..reporting.brief_pack import build_brief_pack_zip_bytes
|
||||||
|
from ..reporting.brief_strategy_scope import (
|
||||||
|
filter_brief_for_strategy_matrix_group,
|
||||||
|
list_matrix_groups_for_api,
|
||||||
|
resolve_strategy_matrix_group_index,
|
||||||
|
)
|
||||||
from ..reporting.md_document_export import markdown_to_docx_bytes, markdown_to_pdf_bytes
|
from ..reporting.md_document_export import markdown_to_docx_bytes, markdown_to_pdf_bytes
|
||||||
from ..reporting.report_strategy_excerpt import load_report_strategy_excerpt
|
from ..reporting.report_strategy_excerpt import load_report_strategy_excerpt
|
||||||
from ..reporting.strategy_draft import build_strategy_draft_markdown
|
from ..reporting.strategy_draft import build_strategy_draft_markdown
|
||||||
@ -57,6 +63,9 @@ class JobCompetitorBriefView(APIView):
|
|||||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = dict(data)
|
||||||
|
data["matrix_groups"] = list_matrix_groups_for_api(data)
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
@ -155,6 +164,26 @@ class JobStrategyDraftView(APIView):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
matrix_groups = list_matrix_groups_for_api(brief)
|
||||||
|
sg_idx = vd.get("strategy_matrix_group_index")
|
||||||
|
sg_lbl = (vd.get("strategy_matrix_group") or "").strip()
|
||||||
|
scope_idx, scope_err = resolve_strategy_matrix_group_index(
|
||||||
|
brief,
|
||||||
|
matrix_group_index=sg_idx,
|
||||||
|
matrix_group_label=sg_lbl or None,
|
||||||
|
)
|
||||||
|
if scope_err:
|
||||||
|
return Response({"detail": scope_err}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
strategy_scope_applied: dict[str, Any] | None = None
|
||||||
|
if scope_idx is not None:
|
||||||
|
brief = filter_brief_for_strategy_matrix_group(
|
||||||
|
brief, matrix_group_index=scope_idx
|
||||||
|
)
|
||||||
|
raw_sa = brief.get("strategy_scope_applied")
|
||||||
|
strategy_scope_applied = (
|
||||||
|
raw_sa if isinstance(raw_sa, dict) else None
|
||||||
|
)
|
||||||
|
|
||||||
gen_at = timezone.now().isoformat()
|
gen_at = timezone.now().isoformat()
|
||||||
generator = (vd.get("generator") or "rules").strip()
|
generator = (vd.get("generator") or "rules").strip()
|
||||||
excerpt_src = "none"
|
excerpt_src = "none"
|
||||||
@ -204,6 +233,8 @@ class JobStrategyDraftView(APIView):
|
|||||||
"markdown": md,
|
"markdown": md,
|
||||||
"report_strategy_excerpt_source": excerpt_src,
|
"report_strategy_excerpt_source": excerpt_src,
|
||||||
"report_strategy_excerpt_chars": len(report_excerpt or ""),
|
"report_strategy_excerpt_chars": len(report_excerpt or ""),
|
||||||
|
"matrix_groups": matrix_groups,
|
||||||
|
"strategy_scope_applied": strategy_scope_applied,
|
||||||
}
|
}
|
||||||
return Response(body)
|
return Response(body)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user