b-sort 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
b_sort/__init__.py ADDED
File without changes
b_sort/blocks.py ADDED
@@ -0,0 +1,22 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ BLOCKS_FILENAME = "blocks.json"
5
+
6
+
7
+ def load_blocks(root: Path | None = None) -> list:
8
+ """加载blocks"""
9
+ root = root or Path.cwd()
10
+ blocks_path = root / BLOCKS_FILENAME
11
+ if not blocks_path.exists():
12
+ return []
13
+
14
+ try:
15
+ overrides = json.loads(blocks_path.read_text(encoding="utf-8"))
16
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
17
+ raise TypeError(f"{BLOCKS_FILENAME} 不是合法 JSON: {exc}") from exc
18
+
19
+ if not isinstance(overrides, list):
20
+ raise TypeError(f"{BLOCKS_FILENAME} 顶层必须是 LIST 对象")
21
+
22
+ return overrides
b_sort/config.py ADDED
@@ -0,0 +1,76 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from pydantic import ValidationError
5
+
6
+ from b_sort.states.sort_state import PipelineConfig, SortState
7
+
8
+
9
+ def resolve_config(state: SortState) -> PipelineConfig:
10
+ """从图状态中取出 PipelineConfig,自动适配三种来源:
11
+
12
+ 1. CLI 入口(`src/graphs/main.py`)通过 ``ainvoke({"config": config})`` 传入真实实例;
13
+ 2. LangGraph Studio 把 ``__start__`` 的 raw input 作为 ``dict`` 透传,``config`` 也是 dict;
14
+ 3. ``__start__`` 未传 ``config`` 时字段缺失。
15
+
16
+ 第 2/3 种情况下回退到 ``PipelineConfig()`` 默认值;若 ``config`` 是 dict,
17
+ 按 ``PipelineConfig.model_fields`` 校验字段并实例化,非法字段让 Pydantic 报错
18
+ 上抛 —— 与 ``src/config.py:load_config`` 的语义一致。
19
+ """
20
+ value = state.get("config")
21
+ if isinstance(value, PipelineConfig):
22
+ return value
23
+ if value is None:
24
+ return PipelineConfig()
25
+ if isinstance(value, dict):
26
+ unknown = set(value) - set(PipelineConfig.model_fields)
27
+ if unknown:
28
+ raise ConfigError(f"{CONFIG_FILENAME} 包含未知字段: {sorted(unknown)}")
29
+ try:
30
+ return PipelineConfig(**value)
31
+ except ValidationError as exc:
32
+ raise ConfigError(f"{CONFIG_FILENAME} 字段非法: {exc}") from exc
33
+ raise ConfigError(
34
+ f"state['config'] 类型不受支持: {type(value).__name__},"
35
+ "应为 PipelineConfig 实例、dict 覆盖或 None"
36
+ )
37
+
38
+
39
+ CONFIG_FILENAME = "config.json"
40
+
41
+
42
+ class ConfigError(Exception):
43
+ """配置文件非法(非法 JSON / 字段类型错误 / 非法取值)。"""
44
+
45
+
46
+ def load_config(root: Path | None = None) -> PipelineConfig:
47
+ """加载流水线配置。
48
+
49
+ - 根目录不存在 config.json 时,返回全默认配置;
50
+ - 存在时,仅覆盖文件中出现的字段;
51
+ - 文件非法时抛出 ConfigError,不静默回退默认值。
52
+
53
+ 未知字段校验直接读 PipelineConfig.model_fields,故新增配置项(如 skills_dir /
54
+ cluster_skill / topics_skill)无需改动本函数即可被覆盖与校验。
55
+ """
56
+ root = root or Path.cwd()
57
+ config_path = root / CONFIG_FILENAME
58
+ if not config_path.exists():
59
+ return PipelineConfig()
60
+
61
+ try:
62
+ overrides = json.loads(config_path.read_text(encoding="utf-8"))
63
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
64
+ raise ConfigError(f"{CONFIG_FILENAME} 不是合法 JSON: {exc}") from exc
65
+
66
+ if not isinstance(overrides, dict):
67
+ raise ConfigError(f"{CONFIG_FILENAME} 顶层必须是 JSON 对象")
68
+
69
+ unknown = set(overrides) - set(PipelineConfig.model_fields)
70
+ if unknown:
71
+ raise ConfigError(f"{CONFIG_FILENAME} 包含未知字段: {sorted(unknown)}")
72
+
73
+ try:
74
+ return PipelineConfig(**overrides)
75
+ except ValidationError as exc:
76
+ raise ConfigError(f"{CONFIG_FILENAME} 字段非法: {exc}") from exc
@@ -0,0 +1,71 @@
1
+ from typing import Any
2
+
3
+ from langgraph.errors import NodeError
4
+ from langgraph.types import Command, RetryPolicy, TimeoutPolicy, default_retry_on
5
+ from pydantic import ValidationError
6
+
7
+ from b_sort.config import ConfigError
8
+ from b_sort.skills import SkillError
9
+
10
+ # 黑名单优先:重试 N 次也必然失败的确定性错误,直接进入降级路径
11
+ _NON_RETRYABLE: tuple[type[Exception], ...] = (
12
+ ConfigError,
13
+ SkillError,
14
+ TypeError,
15
+ ImportError,
16
+ NameError,
17
+ SyntaxError,
18
+ )
19
+
20
+ # 项目白名单:default_retry_on 默认排除这些类型,但在本项目中它们恰是
21
+ # 「LLM 输出不合格(ValidationError)/ 网络抖动(OSError)/ 结构化输出异常(RuntimeError)」
22
+ # 这三类最该重试的瞬时失败
23
+ _RETRYABLE: tuple[type[Exception], ...] = (
24
+ ValidationError,
25
+ RuntimeError,
26
+ OSError,
27
+ TimeoutError,
28
+ )
29
+
30
+
31
+ def pipeline_retry_on(exc: Exception) -> bool:
32
+ """先黑名单、再白名单、最后回落 default_retry_on(NodeTimeoutError 默认可重试)。"""
33
+ if isinstance(exc, _NON_RETRYABLE):
34
+ return False
35
+ if isinstance(exc, _RETRYABLE):
36
+ return True
37
+ return default_retry_on(exc)
38
+
39
+
40
+ DEFAULT_RETRY = RetryPolicy(
41
+ max_attempts=2,
42
+ initial_interval=0.5,
43
+ backoff_factor=2.0,
44
+ max_interval=8.0,
45
+ jitter=True,
46
+ retry_on=pipeline_retry_on,
47
+ )
48
+ DEFAULT_TIMEOUT = TimeoutPolicy(run_timeout=30.0)
49
+
50
+
51
+ LLM_RETRY = RetryPolicy(
52
+ max_attempts=3,
53
+ initial_interval=2.0,
54
+ backoff_factor=2.0,
55
+ max_interval=30.0,
56
+ jitter=True,
57
+ retry_on=pipeline_retry_on,
58
+ )
59
+
60
+ LLM_TIMEOUT = TimeoutPolicy(run_timeout=120.0)
61
+
62
+
63
+ def make_degrade_handler(fallback: dict[str, Any], goto: str):
64
+ """降级处理器工厂:重试耗尽后写告警 + 安全空值产出,并路由到 goto(design D7)。"""
65
+
66
+ def degrade(state: dict[str, Any], error: NodeError) -> Command:
67
+ warning = f"[{error.node}] 重试耗尽后降级: {error.error}"
68
+ print(f"[fault][warn] {warning}")
69
+ return Command(update={**fallback, "warnings": [warning]}, goto=goto)
70
+
71
+ return degrade
File without changes
@@ -0,0 +1,30 @@
1
+ from langgraph.graph import END, START, StateGraph
2
+ from langgraph.types import TimeoutPolicy
3
+
4
+ from b_sort.fault_tolerance import (
5
+ DEFAULT_RETRY,
6
+ LLM_RETRY,
7
+ make_degrade_handler,
8
+ )
9
+ from b_sort.nodes.images_assign import images_assign
10
+ from b_sort.states.sort_state import SortState
11
+
12
+ builder = StateGraph(SortState)
13
+ builder.set_node_defaults(retry_policy=DEFAULT_RETRY)
14
+
15
+
16
+ builder.add_node(
17
+ "images_assign",
18
+ images_assign,
19
+ retry_policy=LLM_RETRY,
20
+ timeout=TimeoutPolicy(run_timeout=300.0),
21
+ error_handler=make_degrade_handler(
22
+ {"image_assigns": []}, goto=END
23
+ ), # 错误handler后面考虑
24
+ )
25
+
26
+
27
+ builder.add_edge(START, "images_assign")
28
+ builder.add_edge("images_assign", END)
29
+
30
+ images_sort_graph = builder.compile()
@@ -0,0 +1,101 @@
1
+ import asyncio
2
+
3
+ from langgraph.graph import END, START, StateGraph
4
+
5
+ from b_sort.fault_tolerance import (
6
+ DEFAULT_RETRY,
7
+ DEFAULT_TIMEOUT,
8
+ make_degrade_handler,
9
+ )
10
+ from b_sort.graphs.images_sort import images_sort_graph
11
+ from b_sort.graphs.paper_sort import paper_sort
12
+ from b_sort.graphs.texts_sort import texts_sort_graph
13
+ from b_sort.nodes.blocks_split import blocks_split
14
+ from b_sort.nodes.data_resolve import data_resolve
15
+ from b_sort.nodes.output_file_json import output_file_json
16
+ from b_sort.nodes.result_clusters import result_clusters
17
+ from b_sort.nodes.texts_cluster_resolve import texts_cluster_resolve
18
+ from b_sort.states.sort_state import SortState
19
+
20
+ builder = StateGraph(SortState)
21
+ builder.set_node_defaults(retry_policy=DEFAULT_RETRY)
22
+ builder.add_node(
23
+ "data_resolve",
24
+ data_resolve,
25
+ retry_policy=DEFAULT_RETRY,
26
+ timeout=DEFAULT_TIMEOUT,
27
+ error_handler=make_degrade_handler(
28
+ {
29
+ "blocks": [],
30
+ },
31
+ goto=END,
32
+ ), # 错误handler后面考虑策略
33
+ )
34
+ builder.add_node(
35
+ "blocks_split",
36
+ blocks_split,
37
+ retry_policy=DEFAULT_RETRY,
38
+ timeout=DEFAULT_TIMEOUT,
39
+ error_handler=make_degrade_handler(
40
+ {"texts": [], "images": []}, goto=END
41
+ ), # 错误handler后面考虑策略
42
+ )
43
+ builder.add_node(
44
+ "texts_sort_graph",
45
+ texts_sort_graph,
46
+ error_handler=make_degrade_handler(
47
+ {"text_clusters": []}, goto=END
48
+ ), # 错误handler后面考虑策略,也通过edge表现出来
49
+ )
50
+ builder.add_node(
51
+ "images_sort_graph",
52
+ images_sort_graph,
53
+ error_handler=make_degrade_handler({"image_assigns": []}, goto=END),
54
+ )
55
+ builder.add_node(
56
+ "texts_cluster_resolve",
57
+ texts_cluster_resolve,
58
+ retry_policy=DEFAULT_RETRY,
59
+ timeout=DEFAULT_TIMEOUT,
60
+ error_handler=make_degrade_handler({"text_clusters_block": []}, goto=END),
61
+ )
62
+ builder.add_node(
63
+ "result_clusters",
64
+ result_clusters,
65
+ retry_policy=DEFAULT_RETRY,
66
+ timeout=DEFAULT_TIMEOUT,
67
+ error_handler=make_degrade_handler({"result_clusters": []}, goto=END),
68
+ )
69
+
70
+ builder.add_node(
71
+ "output_file_json",
72
+ output_file_json,
73
+ retry_policy=DEFAULT_RETRY,
74
+ timeout=DEFAULT_TIMEOUT,
75
+ error_handler=make_degrade_handler({}, goto=END),
76
+ )
77
+ builder.add_edge(START, "data_resolve")
78
+ builder.add_edge("data_resolve", "blocks_split")
79
+ builder.add_edge("blocks_split", "texts_sort_graph")
80
+ builder.add_edge("texts_sort_graph", "texts_cluster_resolve")
81
+ builder.add_edge("texts_cluster_resolve", "images_sort_graph")
82
+ builder.add_edge("images_sort_graph", "result_clusters")
83
+ builder.add_edge("result_clusters", END)
84
+
85
+
86
+ def build_agent(checkpointer):
87
+ """CLI 场景用:注入检查点存储,支持断点续跑。"""
88
+ return builder.compile(checkpointer=checkpointer)
89
+
90
+
91
+ async def paper_cluster(blocks: list | None = None) -> SortState | None:
92
+ result = await paper_sort(blocks=blocks, build_call=build_agent)
93
+ return result if result else None
94
+
95
+
96
+ if __name__ == "__main__":
97
+ from b_sort.blocks import load_blocks
98
+
99
+ blocks = load_blocks()
100
+ result = asyncio.run(paper_cluster(blocks=blocks))
101
+ print("[paper_cluster]", result)
@@ -0,0 +1,176 @@
1
+ from langgraph.graph import END, START, StateGraph
2
+
3
+ from b_sort.fault_tolerance import (
4
+ DEFAULT_RETRY,
5
+ DEFAULT_TIMEOUT,
6
+ make_degrade_handler,
7
+ )
8
+ from b_sort.graphs.images_sort import images_sort_graph
9
+ from b_sort.graphs.texts_sort import texts_sort_graph
10
+ from b_sort.nodes.blocks_split import blocks_split
11
+ from b_sort.nodes.data_resolve import data_resolve
12
+ from b_sort.nodes.output_file_json import output_file_json
13
+ from b_sort.nodes.result_clusters import result_clusters
14
+ from b_sort.nodes.texts_cluster_resolve import texts_cluster_resolve
15
+ from b_sort.states.sort_state import SortState
16
+
17
+ builder = StateGraph(SortState)
18
+ builder.set_node_defaults(retry_policy=DEFAULT_RETRY)
19
+ builder.add_node(
20
+ "data_resolve",
21
+ data_resolve,
22
+ retry_policy=DEFAULT_RETRY,
23
+ timeout=DEFAULT_TIMEOUT,
24
+ error_handler=make_degrade_handler(
25
+ {
26
+ "blocks": [],
27
+ },
28
+ goto=END,
29
+ ), # 错误handler后面考虑策略
30
+ )
31
+ builder.add_node(
32
+ "blocks_split",
33
+ blocks_split,
34
+ retry_policy=DEFAULT_RETRY,
35
+ timeout=DEFAULT_TIMEOUT,
36
+ error_handler=make_degrade_handler(
37
+ {"texts": [], "images": []}, goto=END
38
+ ), # 错误handler后面考虑策略
39
+ )
40
+ builder.add_node(
41
+ "texts_sort_graph",
42
+ texts_sort_graph,
43
+ error_handler=make_degrade_handler(
44
+ {"text_clusters": []}, goto=END
45
+ ), # 错误handler后面考虑策略,也通过edge表现出来
46
+ )
47
+ builder.add_node(
48
+ "images_sort_graph",
49
+ images_sort_graph,
50
+ error_handler=make_degrade_handler({"image_assigns": []}, goto=END),
51
+ )
52
+ builder.add_node(
53
+ "texts_cluster_resolve",
54
+ texts_cluster_resolve,
55
+ retry_policy=DEFAULT_RETRY,
56
+ timeout=DEFAULT_TIMEOUT,
57
+ error_handler=make_degrade_handler({"text_clusters_block": []}, goto=END),
58
+ )
59
+ builder.add_node(
60
+ "result_clusters",
61
+ result_clusters,
62
+ retry_policy=DEFAULT_RETRY,
63
+ timeout=DEFAULT_TIMEOUT,
64
+ error_handler=make_degrade_handler({"result_clusters": []}, goto=END),
65
+ )
66
+
67
+ builder.add_node(
68
+ "output_file_json",
69
+ output_file_json,
70
+ retry_policy=DEFAULT_RETRY,
71
+ timeout=DEFAULT_TIMEOUT,
72
+ error_handler=make_degrade_handler({}, goto=END),
73
+ )
74
+ builder.add_edge(START, "data_resolve")
75
+ builder.add_edge("data_resolve", "blocks_split")
76
+ builder.add_edge("blocks_split", "texts_sort_graph")
77
+ builder.add_edge("texts_sort_graph", "texts_cluster_resolve")
78
+ builder.add_edge("texts_cluster_resolve", "images_sort_graph")
79
+ builder.add_edge("images_sort_graph", "result_clusters")
80
+ builder.add_edge("result_clusters", "output_file_json")
81
+ builder.add_edge("output_file_json", END)
82
+
83
+ agent = builder.compile()
84
+
85
+ import asyncio
86
+ import os
87
+ import signal
88
+ from datetime import UTC, datetime
89
+
90
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
91
+ from langgraph.errors import GraphDrained
92
+ from langgraph.runtime import RunControl
93
+
94
+ from b_sort.blocks import load_blocks
95
+ from b_sort.config import load_config
96
+ from b_sort.skills import load_skill
97
+
98
+
99
+ def _resolve_thread_id(config) -> str:
100
+ """优先 PipelineConfig.thread_id,其次环境变量 TOPIC_THREAD_ID,回落当日日期。"""
101
+ if config.thread_id:
102
+ return config.thread_id
103
+ # 使用本地时间,精确到秒
104
+ now = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d-%H-%M-%S")
105
+ return os.getenv("TOPIC_THREAD_ID") or f"paper-sort-{now}"
106
+
107
+
108
+ def build_agent(checkpointer):
109
+ """CLI 场景用:注入检查点存储,支持断点续跑。"""
110
+ return builder.compile(checkpointer=checkpointer)
111
+
112
+
113
+ from collections.abc import Callable
114
+
115
+ from langgraph.graph.state import CompiledStateGraph
116
+
117
+ from b_sort.states.sort_state import PipelineConfig
118
+
119
+
120
+ async def paper_sort(
121
+ blocks: list | None = None,
122
+ config: PipelineConfig | None = None,
123
+ build_call: Callable[[AsyncSqliteSaver], CompiledStateGraph] | None = None,
124
+ ) -> SortState | None:
125
+ if config is None:
126
+ config = load_config()
127
+ if blocks is None:
128
+ blocks = load_blocks()
129
+
130
+ skill = load_skill(config.skills_dir, config.texts_cluster_skill)
131
+ print(f"[main] skill 就绪 {skill.name}({len(skill.prompt_block)} 字符)")
132
+
133
+ thread_id = _resolve_thread_id(config)
134
+ print(f"[main] thread_id={thread_id} 检查点库={config.checkpoint_db}")
135
+
136
+ control = RunControl()
137
+
138
+ def _on_signal(signum, frame):
139
+ control.request_drain(f"收到信号 {signal.Signals(signum).name}")
140
+
141
+ signal.signal(signal.SIGTERM, _on_signal)
142
+ signal.signal(signal.SIGINT, _on_signal)
143
+
144
+ async with AsyncSqliteSaver.from_conn_string(config.checkpoint_db) as saver:
145
+ if build_call is None:
146
+ graph = build_agent(saver)
147
+ else:
148
+ graph = build_call(saver)
149
+ run_config = {"configurable": {"thread_id": thread_id}}
150
+ snapshot = await graph.aget_state(run_config)
151
+ if snapshot.values:
152
+ # 命中已有检查点:传 None 续跑(已跑完则直接返回旧结果,design D9)
153
+ pending = ", ".join(snapshot.next) if snapshot.next else "无(已跑完)"
154
+ print(
155
+ f"[main] 命中已有检查点(待执行: {pending}),续跑该会话;"
156
+ "如需强制重跑请设置 TOPIC_THREAD_ID"
157
+ )
158
+ input_data = None
159
+ else:
160
+ input_data = {"config": config, "blocks": blocks}
161
+
162
+ try:
163
+ result = await graph.ainvoke(input_data, config=run_config, control=control)
164
+ except GraphDrained:
165
+ print(f"[main] 优雅停机: {control.drain_reason}")
166
+ print(f"[main] 检查点已保存,保持 thread_id={thread_id} 重跑即可续跑")
167
+ return None
168
+
169
+ if control.drain_requested:
170
+ print(f"[main] 停机请求({control.drain_reason})到达时流程恰好已完成")
171
+ print("[main] 完成")
172
+ return result
173
+
174
+
175
+ if __name__ == "__main__":
176
+ asyncio.run(paper_sort())
@@ -0,0 +1,30 @@
1
+ from langgraph.graph import END, START, StateGraph
2
+ from langgraph.types import TimeoutPolicy
3
+
4
+ from b_sort.fault_tolerance import (
5
+ DEFAULT_RETRY,
6
+ LLM_RETRY,
7
+ make_degrade_handler,
8
+ )
9
+ from b_sort.nodes.texts_cluster import texts_cluster
10
+ from b_sort.states.sort_state import SortState
11
+
12
+ builder = StateGraph(SortState)
13
+ builder.set_node_defaults(retry_policy=DEFAULT_RETRY)
14
+
15
+
16
+ builder.add_node(
17
+ "texts_cluster",
18
+ texts_cluster,
19
+ retry_policy=LLM_RETRY,
20
+ timeout=TimeoutPolicy(run_timeout=300.0),
21
+ error_handler=make_degrade_handler(
22
+ {"text_clusters": []}, goto=END
23
+ ), # 错误handler后面考虑
24
+ )
25
+
26
+
27
+ builder.add_edge(START, "texts_cluster")
28
+ builder.add_edge("texts_cluster", END)
29
+
30
+ texts_sort_graph = builder.compile()
b_sort/llm.py ADDED
@@ -0,0 +1,55 @@
1
+ """LLM 模型工厂:ChatOpenAI 指向阿里云 DashScope 兼容模式。"""
2
+
3
+ import os
4
+
5
+ from langchain_core.messages import BaseMessage
6
+ from langchain_openai import ChatOpenAI
7
+ from pydantic import BaseModel
8
+
9
+ DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
10
+
11
+ # SDK 客户端超时取节点 run_timeout(120s)的约 80%,让 SDK 先于节点超时
12
+ # 抛出可归因的网络错误
13
+ # CLIENT_TIMEOUT_SECONDS = 96
14
+
15
+
16
+ def make_chat_model(model: str) -> ChatOpenAI:
17
+ """创建指向 DashScope 兼容模式的模型,API key 只读 BL_TOKEN 环境变量。
18
+
19
+ max_retries=0:重试职责单一归属图层 RetryPolicy,避免 SDK 重试叠乘(design D4)。
20
+ """
21
+ api_key = os.getenv("BL_TOKEN", "").strip()
22
+ if not api_key:
23
+ raise RuntimeError("环境变量 BL_TOKEN 未设置,无法调用 LLM")
24
+ return ChatOpenAI(
25
+ api_key=api_key,
26
+ base_url=DASHSCOPE_BASE_URL,
27
+ model=model,
28
+ # temperature=0.3,
29
+ # timeout=CLIENT_TIMEOUT_SECONDS,
30
+ # max_retries=0,
31
+ )
32
+
33
+
34
+ Prompt = str | list[BaseMessage]
35
+ from typing import TypeVar
36
+
37
+ T = TypeVar("T", bound=BaseModel)
38
+
39
+
40
+ async def ainvoke_structured(model_name: str, schema: type[T], prompt: Prompt) -> T:
41
+ """structured output 单次调用;失败直接抛异常,重试由图层 RetryPolicy 承担。"""
42
+ model = make_chat_model(model_name).with_structured_output(schema)
43
+ result = await model.ainvoke(prompt)
44
+
45
+ # model = make_chat_model(model_name)
46
+
47
+ # async for chunk in model.astream(prompt):
48
+ # print(chunk.content, end="", flush=True)
49
+
50
+ if not isinstance(result, schema):
51
+ # TRY004 不适用:TypeError 在重试黑名单内,此处需 RuntimeError 走图层重试
52
+ raise RuntimeError( # noqa: TRY004
53
+ f"{model_name} 结构化输出类型异常: {type(result)}"
54
+ )
55
+ return result
File without changes
@@ -0,0 +1,23 @@
1
+ from b_sort.states.sort_state import BlockImage, BlockText, SortState
2
+
3
+
4
+ async def blocks_split(state: SortState) -> SortState:
5
+ blocks = state["blocks"]
6
+
7
+ texts: list[BlockText] = []
8
+ images: list[BlockImage] = []
9
+
10
+ print("[blocks_split] start")
11
+ for block in blocks:
12
+ if block.type == "text":
13
+ texts.append(block)
14
+ elif block.type == "image":
15
+ images.append(block)
16
+ else:
17
+ raise ValueError(f"Illegal block: {block}")
18
+
19
+ print("[blocks_split] end")
20
+ return {
21
+ "texts": texts,
22
+ "images": images,
23
+ }
@@ -0,0 +1,28 @@
1
+ from b_sort.config import resolve_config
2
+ from b_sort.states.sort_state import BlockImage, BlockText, SortState
3
+
4
+
5
+ async def data_resolve(state: SortState) -> SortState:
6
+ blocks = state["blocks"]
7
+
8
+ loaded_blocks: list[BlockText | BlockImage] = []
9
+
10
+ print("[data_resolve] start")
11
+ for block in blocks:
12
+ if isinstance(block, dict):
13
+ if block["type"] == "text":
14
+ block = BlockText.model_validate(block)
15
+ elif block["type"] == "image":
16
+ block = BlockImage.model_validate(block)
17
+ else:
18
+ raise ValueError(f"Illegal block: {block}")
19
+
20
+ loaded_blocks.append(block)
21
+
22
+ config = resolve_config(state)
23
+
24
+ print("[data_resolve] end")
25
+ return {
26
+ "config": config,
27
+ "blocks": loaded_blocks,
28
+ }