Skip to content

Commit 9142588

Browse files
authored
Merge pull request #10 from Ontos-AI/bugfix/wangbinqi/chunks-ref-position
refactor: consolidate chunk connection logic and improve parsing in s…
2 parents 60cc0d1 + 72b3ef6 commit 9142588

4 files changed

Lines changed: 266 additions & 282 deletions

File tree

apps/worker/tests/contract/test_parse_task_contract.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,15 @@ def fake_checkerboard_inject_parse(**kwargs: Any) -> tuple[str, pd.DataFrame]:
223223
"summary": "",
224224
"know_id": "kid-1",
225225
"tokens": "",
226-
"connectto": "",
226+
"connectto": json.dumps(
227+
[
228+
{
229+
"target": "table-1",
230+
"relation": "embeds",
231+
"ref": "[tables/table-1.html]",
232+
}
233+
]
234+
),
227235
"addtime": "now",
228236
"page_nums": "1",
229237
},
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""Build canonical chunk connection metadata."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Mapping, Sequence
6+
from typing import Any, TypeAlias, TypedDict
7+
8+
from shared.utils.chunk_refs import ChunkRefSpan, extract_chunk_ref_spans
9+
10+
11+
class PositionPayload(TypedDict):
12+
start: int
13+
end: int
14+
15+
16+
class ConnectionPayload(TypedDict, total=False):
17+
target: str
18+
relation: str
19+
ref: str
20+
position: PositionPayload
21+
score: float
22+
keywords: list[str]
23+
24+
25+
RelationshipRef: TypeAlias = str | ChunkRefSpan
26+
ConnectionValue: TypeAlias = str | ConnectionPayload
27+
ConnectionKey: TypeAlias = tuple[str, str, str]
28+
PositionKey: TypeAlias = tuple[str, str]
29+
30+
31+
def parse_relationship_refs(type_value: object, content: str) -> list[RelationshipRef]:
32+
parsed_relationships = _parse_type_relationship_refs(type_value)
33+
if parsed_relationships:
34+
return [relationship for relationship in parsed_relationships]
35+
return [span for span in extract_chunk_ref_spans(content)]
36+
37+
38+
def build_resource_target_map(
39+
chunks: Sequence[Mapping[str, Any]],
40+
*,
41+
image_files_map: Mapping[str, Mapping[str, Any]] | None = None,
42+
table_files_map: Mapping[str, Mapping[str, Any]] | None = None,
43+
) -> dict[str, str]:
44+
target_map: dict[str, str] = {}
45+
for chunk in chunks:
46+
chunk_id = str(chunk.get("chunk_id") or chunk.get("know_id") or "").strip()
47+
if not chunk_id:
48+
continue
49+
50+
chunk_type = str(chunk.get("type", "")).strip().split("\n", 1)[0].lower()
51+
if chunk_type not in {"image", "table"}:
52+
continue
53+
54+
metadata = chunk.get("metadata", {})
55+
file_path = ""
56+
if isinstance(metadata, dict):
57+
file_path = str(metadata.get("file_path") or "").strip()
58+
if not file_path:
59+
file_map = image_files_map if chunk_type == "image" else table_files_map
60+
file_info = file_map.get(chunk_id) if file_map else None
61+
if file_info:
62+
file_path = str(file_info.get("file_path") or "").strip()
63+
64+
path_alias = str(chunk.get("path") or "").strip()
65+
aliases = {file_path, path_alias}
66+
for alias in list(aliases):
67+
if alias:
68+
aliases.add(f"[{alias}]")
69+
for alias in aliases:
70+
if alias:
71+
target_map[alias] = chunk_id
72+
return target_map
73+
74+
75+
def convert_refs_to_embed_connections(
76+
refs: Sequence[RelationshipRef], target_map: Mapping[str, str]
77+
) -> list[ConnectionPayload]:
78+
connections: list[ConnectionPayload] = []
79+
for ref in refs:
80+
if isinstance(ref, dict):
81+
ref_text = str(ref.get("ref") or "").strip()
82+
start = ref.get("start")
83+
end = ref.get("end")
84+
else:
85+
ref_text = str(ref or "").strip()
86+
start = None
87+
end = None
88+
if not ref_text:
89+
continue
90+
91+
target_id = target_map.get(ref_text)
92+
if not target_id and ref_text.startswith("[") and ref_text.endswith("]"):
93+
target_id = target_map.get(ref_text[1:-1].strip())
94+
if not target_id:
95+
continue
96+
97+
connection: ConnectionPayload = {
98+
"target": target_id,
99+
"relation": "embeds",
100+
"ref": ref_text,
101+
}
102+
if isinstance(start, int) and isinstance(end, int):
103+
connection["position"] = {
104+
"start": start,
105+
"end": end,
106+
}
107+
connections.append(connection)
108+
return connections
109+
110+
111+
def normalize_connect_to_targets(
112+
connects: object, target_map: Mapping[str, str]
113+
) -> list[ConnectionPayload]:
114+
if connects is None or connects == "":
115+
return []
116+
117+
raw_items = connects if isinstance(connects, list) else [connects]
118+
normalized: list[ConnectionPayload] = []
119+
for item in raw_items:
120+
if item is None or item == "":
121+
continue
122+
123+
if isinstance(item, dict):
124+
target = str(item.get("target") or "").strip()
125+
normalized_target = target_map.get(target, target)
126+
if not normalized_target:
127+
continue
128+
129+
normalized_item: ConnectionPayload = {
130+
"target": normalized_target,
131+
"relation": str(item.get("relation") or "related"),
132+
}
133+
score = item.get("score")
134+
if isinstance(score, (int, float)):
135+
normalized_item["score"] = float(score)
136+
keywords = item.get("keywords")
137+
if isinstance(keywords, list):
138+
normalized_item["keywords"] = [str(keyword) for keyword in keywords]
139+
ref = item.get("ref")
140+
if ref:
141+
normalized_item["ref"] = str(ref)
142+
position = item.get("position")
143+
if isinstance(position, dict):
144+
start = position.get("start")
145+
end = position.get("end")
146+
if isinstance(start, int) and isinstance(end, int):
147+
normalized_item["position"] = {"start": start, "end": end}
148+
normalized.append(normalized_item)
149+
continue
150+
151+
target = str(item or "").strip()
152+
normalized_target = target_map.get(target, target)
153+
if normalized_target:
154+
normalized.append(
155+
{
156+
"target": normalized_target,
157+
"relation": "related",
158+
"score": 1.0,
159+
"keywords": [],
160+
}
161+
)
162+
return normalized
163+
164+
165+
def merge_connections(
166+
*connection_lists: Sequence[ConnectionValue],
167+
) -> list[ConnectionValue]:
168+
merged: list[ConnectionValue] = []
169+
unpositioned_indexes: dict[ConnectionKey, int] = {}
170+
positioned_keys: dict[ConnectionKey, set[PositionKey]] = {}
171+
for connection_list in connection_lists:
172+
for item in connection_list or []:
173+
if not isinstance(item, dict):
174+
continue
175+
key = _get_connection_key(item)
176+
position_key = _get_connection_position_key(item)
177+
if position_key is None:
178+
if key in unpositioned_indexes or key in positioned_keys:
179+
continue
180+
unpositioned_indexes[key] = len(merged)
181+
merged.append(item)
182+
continue
183+
184+
key_positions = positioned_keys.setdefault(key, set())
185+
if position_key in key_positions:
186+
continue
187+
key_positions.add(position_key)
188+
unpositioned_index = unpositioned_indexes.pop(key, None)
189+
if unpositioned_index is None:
190+
merged.append(item)
191+
else:
192+
merged[unpositioned_index] = item
193+
return merged
194+
195+
196+
def _parse_type_relationship_refs(type_value: object) -> list[str]:
197+
if not isinstance(type_value, str) or "\n" not in type_value:
198+
return []
199+
lines = [line.strip() for line in type_value.split("\n") if line.strip()]
200+
return [line for line in lines[1:] if line.upper() != "PTXT"]
201+
202+
203+
def _get_connection_key(item: ConnectionValue) -> ConnectionKey:
204+
if not isinstance(item, dict):
205+
return ("", "related", "")
206+
return (
207+
str(item.get("target") or ""),
208+
str(item.get("relation") or "related"),
209+
str(item.get("ref") or ""),
210+
)
211+
212+
213+
def _get_connection_position_key(item: ConnectionValue) -> PositionKey | None:
214+
if not isinstance(item, dict):
215+
return None
216+
position = item.get("position")
217+
if not isinstance(position, dict):
218+
return None
219+
return (
220+
str(position.get("start", "")),
221+
str(position.get("end", "")),
222+
)

0 commit comments

Comments
 (0)