dialectical-framework 0.4.4__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.
- dialectical_framework/__init__.py +43 -0
- dialectical_framework/ai_dto/__init__.py +0 -0
- dialectical_framework/ai_dto/causal_cycle_assessment_dto.py +16 -0
- dialectical_framework/ai_dto/causal_cycle_dto.py +16 -0
- dialectical_framework/ai_dto/causal_cycles_deck_dto.py +11 -0
- dialectical_framework/ai_dto/dialectical_component_dto.py +16 -0
- dialectical_framework/ai_dto/dialectical_components_deck_dto.py +12 -0
- dialectical_framework/ai_dto/dto_mapper.py +103 -0
- dialectical_framework/ai_dto/reciprocal_solution_dto.py +24 -0
- dialectical_framework/analyst/__init__.py +1 -0
- dialectical_framework/analyst/audit/__init__.py +0 -0
- dialectical_framework/analyst/consultant.py +30 -0
- dialectical_framework/analyst/decorator_action_reflection.py +28 -0
- dialectical_framework/analyst/decorator_discrete_spiral.py +30 -0
- dialectical_framework/analyst/domain/__init__.py +0 -0
- dialectical_framework/analyst/domain/assessable_cycle.py +128 -0
- dialectical_framework/analyst/domain/cycle.py +133 -0
- dialectical_framework/analyst/domain/interpretation.py +16 -0
- dialectical_framework/analyst/domain/rationale.py +116 -0
- dialectical_framework/analyst/domain/spiral.py +47 -0
- dialectical_framework/analyst/domain/transformation.py +24 -0
- dialectical_framework/analyst/domain/transition.py +160 -0
- dialectical_framework/analyst/domain/transition_cell_to_cell.py +29 -0
- dialectical_framework/analyst/domain/transition_segment_to_segment.py +16 -0
- dialectical_framework/analyst/strategic_consultant.py +32 -0
- dialectical_framework/analyst/think_action_reflection.py +217 -0
- dialectical_framework/analyst/think_constructive_convergence.py +87 -0
- dialectical_framework/analyst/wheel_builder_transition_calculator.py +157 -0
- dialectical_framework/brain.py +81 -0
- dialectical_framework/dialectical_analysis.py +14 -0
- dialectical_framework/dialectical_component.py +111 -0
- dialectical_framework/dialectical_components_deck.py +48 -0
- dialectical_framework/dialectical_reasoning.py +105 -0
- dialectical_framework/directed_graph.py +419 -0
- dialectical_framework/enums/__init__.py +0 -0
- dialectical_framework/enums/causality_type.py +10 -0
- dialectical_framework/enums/di.py +11 -0
- dialectical_framework/enums/dialectical_reasoning_mode.py +9 -0
- dialectical_framework/enums/predicate.py +9 -0
- dialectical_framework/protocols/__init__.py +0 -0
- dialectical_framework/protocols/assessable.py +141 -0
- dialectical_framework/protocols/causality_sequencer.py +111 -0
- dialectical_framework/protocols/content_fidelity_evaluator.py +16 -0
- dialectical_framework/protocols/has_brain.py +18 -0
- dialectical_framework/protocols/has_config.py +18 -0
- dialectical_framework/protocols/ratable.py +79 -0
- dialectical_framework/protocols/reloadable.py +7 -0
- dialectical_framework/protocols/thesis_extractor.py +15 -0
- dialectical_framework/settings.py +77 -0
- dialectical_framework/synthesis.py +7 -0
- dialectical_framework/synthesist/__init__.py +1 -0
- dialectical_framework/synthesist/causality/__init__.py +0 -0
- dialectical_framework/synthesist/causality/causality_sequencer_balanced.py +398 -0
- dialectical_framework/synthesist/causality/causality_sequencer_desirable.py +55 -0
- dialectical_framework/synthesist/causality/causality_sequencer_feasible.py +55 -0
- dialectical_framework/synthesist/causality/causality_sequencer_realistic.py +56 -0
- dialectical_framework/synthesist/concepts/__init__.py +0 -0
- dialectical_framework/synthesist/concepts/thesis_extractor_basic.py +135 -0
- dialectical_framework/synthesist/polarity/__init__.py +0 -0
- dialectical_framework/synthesist/polarity/polarity_reasoner.py +700 -0
- dialectical_framework/synthesist/polarity/reason_blind.py +62 -0
- dialectical_framework/synthesist/polarity/reason_conversational.py +91 -0
- dialectical_framework/synthesist/polarity/reason_fast.py +177 -0
- dialectical_framework/synthesist/polarity/reason_fast_and_simple.py +52 -0
- dialectical_framework/synthesist/polarity/reason_fast_polarized_conflict.py +55 -0
- dialectical_framework/synthesist/reverse_engineer.py +401 -0
- dialectical_framework/synthesist/wheel_builder.py +337 -0
- dialectical_framework/utils/__init__.py +1 -0
- dialectical_framework/utils/dc_replace.py +42 -0
- dialectical_framework/utils/decompose_probability_uniformly.py +15 -0
- dialectical_framework/utils/extend_tpl.py +12 -0
- dialectical_framework/utils/gm.py +12 -0
- dialectical_framework/utils/is_async.py +13 -0
- dialectical_framework/utils/use_brain.py +80 -0
- dialectical_framework/validator/__init__.py +1 -0
- dialectical_framework/validator/basic_checks.py +75 -0
- dialectical_framework/validator/check.py +12 -0
- dialectical_framework/wheel.py +395 -0
- dialectical_framework/wheel_segment.py +203 -0
- dialectical_framework/wisdom_unit.py +185 -0
- dialectical_framework-0.4.4.dist-info/LICENSE +21 -0
- dialectical_framework-0.4.4.dist-info/METADATA +123 -0
- dialectical_framework-0.4.4.dist-info/RECORD +84 -0
- dialectical_framework-0.4.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,395 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import List, Union, Any, Dict
|
4
|
+
|
5
|
+
from tabulate import tabulate
|
6
|
+
|
7
|
+
from dialectical_framework.analyst.domain.cycle import Cycle
|
8
|
+
from dialectical_framework.analyst.domain.spiral import Spiral
|
9
|
+
from dialectical_framework.analyst.domain.transition import Transition
|
10
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
11
|
+
from dialectical_framework.protocols.assessable import Assessable
|
12
|
+
from dialectical_framework.utils.gm import gm_with_zeros_and_nones_handled
|
13
|
+
from dialectical_framework.wheel_segment import WheelSegment
|
14
|
+
from dialectical_framework.wisdom_unit import WisdomUnit
|
15
|
+
|
16
|
+
WheelSegmentReference = Union[int, WheelSegment, str, DialecticalComponent]
|
17
|
+
|
18
|
+
|
19
|
+
class Wheel(Assessable):
|
20
|
+
def __init__(self, *wisdom_units, t_cycle: Cycle, ta_cycle: Cycle, **kwargs):
|
21
|
+
super().__init__(**kwargs)
|
22
|
+
|
23
|
+
# One iterable argument → use it directly
|
24
|
+
if len(wisdom_units) == 1 and not isinstance(wisdom_units[0], WisdomUnit):
|
25
|
+
self._wisdom_units: list[WisdomUnit] = list(wisdom_units[0])
|
26
|
+
else:
|
27
|
+
self._wisdom_units: list[WisdomUnit] = list(wisdom_units)
|
28
|
+
|
29
|
+
self._ta_cycle: Cycle = ta_cycle
|
30
|
+
self._t_cycle: Cycle = t_cycle
|
31
|
+
self._spiral: Spiral = Spiral()
|
32
|
+
|
33
|
+
@property
|
34
|
+
def order(self) -> int:
|
35
|
+
"""The order of the wheel (number of wisdom units in the dialectical structure)"""
|
36
|
+
if len(self._wisdom_units) == 0:
|
37
|
+
raise ValueError("The wheel is empty, therefore order is undefined.")
|
38
|
+
return len(self._wisdom_units)
|
39
|
+
|
40
|
+
@property
|
41
|
+
def degree(self) -> int:
|
42
|
+
"""The degree of the wheel (total number of segments = 2 × order)"""
|
43
|
+
return self.order * 2
|
44
|
+
|
45
|
+
def _get_sub_assessables(self) -> list[Assessable]:
|
46
|
+
result = super()._get_sub_assessables()
|
47
|
+
result.extend(self._wisdom_units)
|
48
|
+
result.append(self._t_cycle)
|
49
|
+
result.append(self._ta_cycle)
|
50
|
+
result.append(self._spiral)
|
51
|
+
return result
|
52
|
+
|
53
|
+
def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
|
54
|
+
"""
|
55
|
+
Calculates the WheelFidelity as the geometric mean of:
|
56
|
+
1. All individual DialecticalComponent contextual fidelity scores across the entire wheel
|
57
|
+
2. All wheel-level rationales/opinions (weighted by their rating)
|
58
|
+
|
59
|
+
Components with contextual_fidelity of 0.0 or None are excluded from the calculation.
|
60
|
+
"""
|
61
|
+
parts = []
|
62
|
+
|
63
|
+
# Collect from wisdom units
|
64
|
+
for wu in self._wisdom_units:
|
65
|
+
fidelity = wu.calculate_contextual_fidelity(mutate=mutate)
|
66
|
+
parts.append(fidelity)
|
67
|
+
|
68
|
+
# Collect transitions from cycles without overlaps
|
69
|
+
unique_transitions: Dict[Any, Transition] = {}
|
70
|
+
|
71
|
+
# Get transitions from t_cycle (most generic)
|
72
|
+
if self._t_cycle.graph and not self._t_cycle.graph.is_empty():
|
73
|
+
for transition in self._t_cycle.graph.get_all_transitions():
|
74
|
+
unique_transitions[transition.get_key()] = transition
|
75
|
+
|
76
|
+
# Get transitions from ta_cycle (more specific than t_cycle, so we prefer this)
|
77
|
+
if self._ta_cycle.graph and not self._ta_cycle.graph.is_empty():
|
78
|
+
for transition in self._ta_cycle.graph.get_all_transitions():
|
79
|
+
if transition.get_key() in unique_transitions:
|
80
|
+
ta_fidelity = transition.calculate_contextual_fidelity(mutate=mutate)
|
81
|
+
if ta_fidelity == 1.0:
|
82
|
+
# no effect, we can stick to moe generic level, whatever fidelity will be there, it will be (more) correct
|
83
|
+
pass
|
84
|
+
else:
|
85
|
+
# calculate the one which is being overwritten
|
86
|
+
unique_transitions[transition.get_key()].calculate_contextual_fidelity(mutate=mutate)
|
87
|
+
# take the more specific one
|
88
|
+
unique_transitions[transition.get_key()] = transition
|
89
|
+
|
90
|
+
# Get transitions from spiral (even more specific, than ta_cycle, so we prefer this)
|
91
|
+
if self._spiral.graph and not self._spiral.graph.is_empty():
|
92
|
+
for transition in self._spiral.graph.get_all_transitions():
|
93
|
+
if transition.get_key() in unique_transitions:
|
94
|
+
sp_fidelity = transition.calculate_contextual_fidelity(mutate=mutate)
|
95
|
+
if sp_fidelity == 1.0:
|
96
|
+
# no effect, we can stick to moe generic level, whatever fidelity will be there, it will be (more) correct
|
97
|
+
pass
|
98
|
+
else:
|
99
|
+
# calculate the one which is being overwritten
|
100
|
+
unique_transitions[transition.get_key()].calculate_contextual_fidelity(mutate=mutate)
|
101
|
+
# take the more specific one
|
102
|
+
unique_transitions[transition.get_key()] = transition
|
103
|
+
|
104
|
+
# Extract fidelity scores from unique transitions
|
105
|
+
for transition in unique_transitions.values():
|
106
|
+
transition_fidelity = transition.calculate_contextual_fidelity(mutate=mutate)
|
107
|
+
if transition_fidelity is not None and transition_fidelity > 0.0:
|
108
|
+
parts.append(transition_fidelity)
|
109
|
+
|
110
|
+
|
111
|
+
return parts
|
112
|
+
|
113
|
+
def calculate_probability(self, *, mutate: bool = True) -> float | None:
|
114
|
+
"""
|
115
|
+
Wheel Pr = GM over a fixed canonical set of cycle probabilities (T, TA, Spiral).
|
116
|
+
- Keep 0.0 (impossibility ⇒ 0).
|
117
|
+
- Skip None (unknown).
|
118
|
+
- If all canonical cycles are None ⇒ None.
|
119
|
+
Optionally add a single summarized term for unit transformations (see note).
|
120
|
+
"""
|
121
|
+
# 1) Canonical cycles (ensure these are EXTERNAL transitions only)
|
122
|
+
canonical_vals = []
|
123
|
+
for cyc in (self._t_cycle, self._ta_cycle, self._spiral):
|
124
|
+
p = cyc.calculate_probability(mutate=mutate)
|
125
|
+
if p is not None:
|
126
|
+
canonical_vals.append(p)
|
127
|
+
|
128
|
+
# 2) Optional: summarize unit transformations ONCE (only if not already in canonical cycles)
|
129
|
+
internal_summary = None
|
130
|
+
unit_vals = []
|
131
|
+
for wu in self._wisdom_units:
|
132
|
+
p = wu.calculate_probability(mutate=mutate) # Pr of the unit’s internal 2-edge Transformation
|
133
|
+
if p is not None:
|
134
|
+
unit_vals.append(p)
|
135
|
+
if unit_vals:
|
136
|
+
internal_summary = gm_with_zeros_and_nones_handled(unit_vals)
|
137
|
+
|
138
|
+
# Build final list (omit internal_summary if your canonical cycles include internal edges)
|
139
|
+
all_terms = list(canonical_vals)
|
140
|
+
if internal_summary is not None:
|
141
|
+
all_terms.append(internal_summary)
|
142
|
+
|
143
|
+
probability = gm_with_zeros_and_nones_handled(all_terms)
|
144
|
+
|
145
|
+
if mutate:
|
146
|
+
self.probability = probability
|
147
|
+
return probability
|
148
|
+
|
149
|
+
@property
|
150
|
+
def wisdom_units(self) -> list[WisdomUnit]:
|
151
|
+
return self._wisdom_units
|
152
|
+
|
153
|
+
@property
|
154
|
+
def main_wisdom_unit(self) -> WisdomUnit:
|
155
|
+
if len(self._wisdom_units) > 0:
|
156
|
+
return self._wisdom_units[0]
|
157
|
+
else:
|
158
|
+
raise ValueError("The wheel is empty.")
|
159
|
+
|
160
|
+
def is_set(self, s: str | DialecticalComponent | WheelSegment) -> bool:
|
161
|
+
try:
|
162
|
+
self.wisdom_unit_at(s)
|
163
|
+
except ValueError:
|
164
|
+
return False
|
165
|
+
else:
|
166
|
+
return True
|
167
|
+
|
168
|
+
def is_same_structure(self, other: Wheel) -> bool:
|
169
|
+
if len(self.wisdom_units) != len(other.wisdom_units):
|
170
|
+
return False
|
171
|
+
for wu in self.wisdom_units:
|
172
|
+
if not other.is_set(wu):
|
173
|
+
return False
|
174
|
+
|
175
|
+
return self.t_cycle.is_same_structure(
|
176
|
+
other.t_cycle
|
177
|
+
) and self.cycle.is_same_structure(other.cycle)
|
178
|
+
|
179
|
+
def wisdom_unit_at(self, i: WheelSegmentReference) -> WisdomUnit:
|
180
|
+
"""
|
181
|
+
Determines and retrieves a WisdomUnit based on the input index or key.
|
182
|
+
|
183
|
+
This method identifies and returns a specific WisdomUnit from the collection
|
184
|
+
of WisdomUnits maintained by the object. The input can be provided as an integer
|
185
|
+
index, string key, or an instance of the WheelSegment, with distinct lookup
|
186
|
+
logic applied for each type. If the input does not correspond to
|
187
|
+
a valid WisdomUnit, an exception is raised.
|
188
|
+
|
189
|
+
Parameters:
|
190
|
+
i : int | str | WheelSegment
|
191
|
+
The input used to locate a specific WisdomUnit. Can be an integer
|
192
|
+
index of a wisdom unit, a string alias of a dialectical component, or an instance of the WheelSegment.
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
WisdomUnit
|
196
|
+
The WisdomUnit corresponding to the provided input.
|
197
|
+
|
198
|
+
Raises:
|
199
|
+
IndexError
|
200
|
+
If the integer input is out of range for the collection of WisdomUnits.
|
201
|
+
ValueError
|
202
|
+
If the input of type WheelSegment or string does not correspond to
|
203
|
+
a valid WisdomUnit in the collection.
|
204
|
+
"""
|
205
|
+
if isinstance(i, WisdomUnit):
|
206
|
+
for wu in self.wisdom_units:
|
207
|
+
if wu.is_same(i):
|
208
|
+
return wu
|
209
|
+
elif isinstance(i, WheelSegment):
|
210
|
+
for wu in self.wisdom_units:
|
211
|
+
if wu.extract_segment_t().is_same(i) or wu.extract_segment_a().is_same(
|
212
|
+
i
|
213
|
+
):
|
214
|
+
return wu
|
215
|
+
raise ValueError(f"Cannot find wisdom unit at: {i.t.alias}")
|
216
|
+
elif isinstance(i, str) or isinstance(i, DialecticalComponent):
|
217
|
+
for wu in self.wisdom_units:
|
218
|
+
if wu.is_set(i):
|
219
|
+
return wu
|
220
|
+
elif isinstance(i, int):
|
221
|
+
if i < 0 or i >= len(self.wisdom_units):
|
222
|
+
raise IndexError(
|
223
|
+
f"index {i} out of range for wheel of length {len(self.wisdom_units)}"
|
224
|
+
)
|
225
|
+
return self.wisdom_units[i]
|
226
|
+
|
227
|
+
raise ValueError(f"Cannot find wisdom unit at: {i}")
|
228
|
+
|
229
|
+
def wheel_segment_at(self, i: int | str | DialecticalComponent) -> WheelSegment:
|
230
|
+
"""
|
231
|
+
Retrieves a specific wheel segment from the wisdom units based on the provided index or component.
|
232
|
+
|
233
|
+
The method allows accessing a wheel segment by an integer index, a string, or a DialecticalComponent.
|
234
|
+
If an integer index is provided, it is validated to ensure it lies within the appropriate range and then
|
235
|
+
used to determine the specific segment from a sequence of wisdom units. For string or DialecticalComponent
|
236
|
+
inputs, the method searches through the wisdom units to locate and return the segment containing the
|
237
|
+
specified component.
|
238
|
+
|
239
|
+
Raises:
|
240
|
+
IndexError: If the integer index is out of the valid range.
|
241
|
+
ValueError: If no wisdom unit or segment corresponding to the given identifier is found.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
i: The index of the wheel segment as an integer or the identifier of the component
|
245
|
+
as a string or DialecticalComponent.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
The matching WheelSegment extracted from the appropriate wisdom unit.
|
249
|
+
"""
|
250
|
+
if isinstance(i, int):
|
251
|
+
total_segments = self.degree
|
252
|
+
if i < 0 or i >= total_segments:
|
253
|
+
raise IndexError(
|
254
|
+
f"index {i} out of range for wheel of {total_segments} segments"
|
255
|
+
)
|
256
|
+
wu_index = i % self.order
|
257
|
+
wu = self.wisdom_units[wu_index]
|
258
|
+
return wu.extract_segment_t() if i < self.order else wu.extract_segment_a()
|
259
|
+
elif isinstance(i, str) or isinstance(i, DialecticalComponent):
|
260
|
+
for wu in self.wisdom_units:
|
261
|
+
if wu.is_set(i):
|
262
|
+
segment_t = wu.extract_segment_t()
|
263
|
+
if segment_t.is_set(i):
|
264
|
+
return segment_t
|
265
|
+
segment_a = wu.extract_segment_a()
|
266
|
+
if segment_a.is_set(i):
|
267
|
+
return segment_a
|
268
|
+
raise ValueError(f"Cannot find wisdom unit at: {i}")
|
269
|
+
|
270
|
+
@property
|
271
|
+
def orthogonal_wisdom_unit(self) -> WisdomUnit:
|
272
|
+
"""
|
273
|
+
Raises:
|
274
|
+
ValueError: If the number of segments is not even.
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
WisdomUnit: The orthogonal segment
|
278
|
+
"""
|
279
|
+
n = len(self._wisdom_units)
|
280
|
+
if n == 0:
|
281
|
+
raise ValueError(
|
282
|
+
"The wheel is empty, therefore no orthogonal segment exists."
|
283
|
+
)
|
284
|
+
if n % 2 == 0:
|
285
|
+
return self._wisdom_units[n // 2]
|
286
|
+
else:
|
287
|
+
raise ValueError("The wheel is not balanced orthogonally.")
|
288
|
+
|
289
|
+
def spin(
|
290
|
+
self,
|
291
|
+
offset: int = 1,
|
292
|
+
) -> list[WisdomUnit]:
|
293
|
+
# TODO: do we ned to also adjust the cycle and spiral?
|
294
|
+
"""
|
295
|
+
Rotate the synthesis-pair list by ``offset`` positions.
|
296
|
+
|
297
|
+
Parameters
|
298
|
+
----------
|
299
|
+
offset : int
|
300
|
+
How far to rotate. Positive values rotate left; negative values
|
301
|
+
rotate right.
|
302
|
+
"""
|
303
|
+
n = len(self._wisdom_units)
|
304
|
+
if n == 0:
|
305
|
+
raise ValueError("Cannot spin an empty wheel")
|
306
|
+
|
307
|
+
if not -n <= offset < n:
|
308
|
+
raise IndexError(
|
309
|
+
f"spin offset {offset} out of range for list of length {n}"
|
310
|
+
)
|
311
|
+
|
312
|
+
offset %= n # bring offset into the list's range
|
313
|
+
|
314
|
+
def rot(lst: List) -> List:
|
315
|
+
return lst[offset:] + lst[:offset]
|
316
|
+
|
317
|
+
self._wisdom_units[:] = rot(self._wisdom_units)
|
318
|
+
|
319
|
+
return self._wisdom_units
|
320
|
+
|
321
|
+
@property
|
322
|
+
def cycle(self) -> Cycle:
|
323
|
+
return self._ta_cycle
|
324
|
+
|
325
|
+
@property
|
326
|
+
def t_cycle(self) -> Cycle:
|
327
|
+
return self._t_cycle
|
328
|
+
|
329
|
+
@property
|
330
|
+
def spiral(self) -> Spiral:
|
331
|
+
return self._spiral
|
332
|
+
|
333
|
+
def index_of(self, ws: WheelSegment) -> int:
|
334
|
+
for i, wu in enumerate(self._wisdom_units):
|
335
|
+
if wu.extract_segment_t().is_same(ws):
|
336
|
+
return i
|
337
|
+
if wu.extract_segment_a().is_same(ws):
|
338
|
+
return i + self.order
|
339
|
+
# Should never happen
|
340
|
+
return -1
|
341
|
+
|
342
|
+
def __str__(self):
|
343
|
+
main_segment = self.main_wisdom_unit
|
344
|
+
output = (
|
345
|
+
"\n---\n"
|
346
|
+
+ self.t_cycle.pretty(
|
347
|
+
skip_dialectical_component_explanation=True, start_alias=main_segment.t
|
348
|
+
)
|
349
|
+
+ "\n---\n"
|
350
|
+
+ "\n---\n"
|
351
|
+
+ self.cycle.pretty(
|
352
|
+
skip_dialectical_component_explanation=True, start_alias=main_segment.t
|
353
|
+
)
|
354
|
+
+ "\n---\n"
|
355
|
+
+ self._print_wheel_tabular()
|
356
|
+
+ "\n---\n"
|
357
|
+
+ self.spiral.pretty(start_wheel_segment=main_segment)
|
358
|
+
+ "\n---\n"
|
359
|
+
)
|
360
|
+
|
361
|
+
return output
|
362
|
+
|
363
|
+
def _print_wheel_tabular(self) -> str:
|
364
|
+
roles = [
|
365
|
+
("t_minus", "T-"),
|
366
|
+
("t", "T"),
|
367
|
+
("t_plus", "T+"),
|
368
|
+
("a_plus", "A+"),
|
369
|
+
("a", "A"),
|
370
|
+
("a_minus", "A-"),
|
371
|
+
]
|
372
|
+
|
373
|
+
n_units = len(self._wisdom_units)
|
374
|
+
|
375
|
+
# Create headers: WU1_alias, WU1_statement, (transition1), WU2_alias, ...
|
376
|
+
headers = []
|
377
|
+
for i in range(n_units):
|
378
|
+
headers.extend([f"Alias (WU{i + 1})", f"Statement (WU{i + 1})"])
|
379
|
+
|
380
|
+
table = []
|
381
|
+
# Build the table: alternate wisdom unit cells and transitions
|
382
|
+
for role_attr, role_label in roles:
|
383
|
+
row = []
|
384
|
+
for i, wu in enumerate(self._wisdom_units):
|
385
|
+
# Wisdom unit columns
|
386
|
+
component = getattr(wu, role_attr, None)
|
387
|
+
row.append(component.alias if component else "")
|
388
|
+
row.append(component.statement if component else "")
|
389
|
+
table.append(row)
|
390
|
+
|
391
|
+
return tabulate(
|
392
|
+
table,
|
393
|
+
# headers=headers,
|
394
|
+
tablefmt="plain",
|
395
|
+
)
|
@@ -0,0 +1,203 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import ConfigDict, Field
|
4
|
+
from pydantic.fields import FieldInfo
|
5
|
+
|
6
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
7
|
+
from dialectical_framework.protocols.assessable import Assessable
|
8
|
+
|
9
|
+
ALIAS_T = "T"
|
10
|
+
ALIAS_T_PLUS = "T+"
|
11
|
+
ALIAS_T_MINUS = "T-"
|
12
|
+
|
13
|
+
|
14
|
+
class WheelSegment(Assessable):
|
15
|
+
|
16
|
+
model_config = ConfigDict(
|
17
|
+
extra="forbid",
|
18
|
+
populate_by_name=True,
|
19
|
+
)
|
20
|
+
|
21
|
+
def __setattr__(self, name, value):
|
22
|
+
# If the attribute name is an alias, use the corresponding field name
|
23
|
+
if name in self.alias_to_field:
|
24
|
+
super().__setattr__(self.alias_to_field[name], value)
|
25
|
+
else:
|
26
|
+
# Otherwise use the default behavior
|
27
|
+
super().__setattr__(name, value)
|
28
|
+
|
29
|
+
t_minus: DialecticalComponent | None = Field(
|
30
|
+
default=None,
|
31
|
+
description="The negative side of the thesis: T-",
|
32
|
+
alias=ALIAS_T_MINUS,
|
33
|
+
)
|
34
|
+
t: DialecticalComponent | None = Field(
|
35
|
+
default=None, description="The major thesis of the input: T", alias=ALIAS_T
|
36
|
+
)
|
37
|
+
t_plus: DialecticalComponent | None = Field(
|
38
|
+
default=None,
|
39
|
+
description="The positive side of the thesis: T+",
|
40
|
+
alias=ALIAS_T_PLUS,
|
41
|
+
)
|
42
|
+
|
43
|
+
def _get_sub_assessables(self) -> list[Assessable]:
|
44
|
+
result = super()._get_sub_assessables()
|
45
|
+
if self.t:
|
46
|
+
result.append(self.t)
|
47
|
+
if self.t_minus:
|
48
|
+
result.append(self.t_minus)
|
49
|
+
if self.t_plus:
|
50
|
+
result.append(self.t_plus)
|
51
|
+
return result
|
52
|
+
|
53
|
+
def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
|
54
|
+
"""
|
55
|
+
Calculates the context fidelity score for this wheel segment as the geometric mean
|
56
|
+
of its constituent DialecticalComponent's scores, including those from its synthesis,
|
57
|
+
and weighted rationale opinions.
|
58
|
+
Components with a context_fidelity_score of 0.0 or None are excluded from the calculation.
|
59
|
+
"""
|
60
|
+
parts = []
|
61
|
+
|
62
|
+
# Collect from dialectical components
|
63
|
+
for f in self.field_to_alias.keys():
|
64
|
+
dc = getattr(self, f)
|
65
|
+
if isinstance(dc, DialecticalComponent):
|
66
|
+
fidelity = dc.calculate_contextual_fidelity(mutate=mutate)
|
67
|
+
parts.append(fidelity)
|
68
|
+
|
69
|
+
return parts
|
70
|
+
|
71
|
+
def calculate_probability(self, *, mutate: bool = True) -> float | None:
|
72
|
+
"""
|
73
|
+
There are no transitions, so we treat it as a fact
|
74
|
+
"""
|
75
|
+
|
76
|
+
# A statement is a fact, so probability is always 1.0
|
77
|
+
probability = self.probability if self.probability is not None else 1.0
|
78
|
+
|
79
|
+
if mutate:
|
80
|
+
self.probability = probability
|
81
|
+
|
82
|
+
return probability
|
83
|
+
|
84
|
+
def _get_dialectical_fields(self) -> dict[str, FieldInfo]:
|
85
|
+
"""Get only fields that contain DialecticalComponent instances and have aliases."""
|
86
|
+
return {
|
87
|
+
field_name: field_info
|
88
|
+
for field_name, field_info in self.__pydantic_fields__.items()
|
89
|
+
if hasattr(field_info, "alias") and field_info.alias is not None
|
90
|
+
}
|
91
|
+
|
92
|
+
def is_complete(self):
|
93
|
+
return all(v is not None for v in self.model_dump(exclude_none=False).values())
|
94
|
+
|
95
|
+
def is_same(self, other: WheelSegment) -> bool:
|
96
|
+
if self == other:
|
97
|
+
return True
|
98
|
+
if type(self) != type(other):
|
99
|
+
return False
|
100
|
+
|
101
|
+
for field in self.alias_to_field.values():
|
102
|
+
dc_self = self.get(field)
|
103
|
+
dc_other = other.get(field)
|
104
|
+
if dc_self is None and dc_other is None:
|
105
|
+
continue
|
106
|
+
if dc_self is None or dc_other is None:
|
107
|
+
return False
|
108
|
+
if not dc_self.is_same(dc_other):
|
109
|
+
return False
|
110
|
+
return True
|
111
|
+
|
112
|
+
def is_set(self, key: str | DialecticalComponent) -> bool:
|
113
|
+
"""
|
114
|
+
True if the given field/alias exists **and** its value is not ``None``.
|
115
|
+
>>> ws = WheelSegment()
|
116
|
+
>>> ws.is_set("T")
|
117
|
+
>>> ws.is_set("t")
|
118
|
+
"""
|
119
|
+
if isinstance(key, DialecticalComponent):
|
120
|
+
return (
|
121
|
+
any(
|
122
|
+
getattr(self, field).is_same(key)
|
123
|
+
for field in self.alias_to_field.values()
|
124
|
+
if getattr(self, field) is not None
|
125
|
+
)
|
126
|
+
or self.get(key.alias, None) is not None
|
127
|
+
)
|
128
|
+
else:
|
129
|
+
return self.get(key, None) is not None
|
130
|
+
|
131
|
+
def get(
|
132
|
+
self, key: str, default: object | None = None
|
133
|
+
) -> DialecticalComponent | None:
|
134
|
+
"""
|
135
|
+
Dictionary-style accessor that understands both *field names* and *aliases*.
|
136
|
+
|
137
|
+
Examples
|
138
|
+
--------
|
139
|
+
>>> ws = WheelSegment()
|
140
|
+
>>> ws.get("t") # by field name
|
141
|
+
>>> ws.get("T") # by alias
|
142
|
+
"""
|
143
|
+
field_name: str = self.alias_to_field.get(key, key)
|
144
|
+
if hasattr(self, field_name):
|
145
|
+
value = getattr(self, field_name)
|
146
|
+
return value if value is not None else default
|
147
|
+
return default
|
148
|
+
|
149
|
+
@property
|
150
|
+
def field_to_alias(self) -> dict[str, str]:
|
151
|
+
return {
|
152
|
+
field_name: field_info.alias
|
153
|
+
for field_name, field_info in self._get_dialectical_fields().items()
|
154
|
+
}
|
155
|
+
|
156
|
+
@property
|
157
|
+
def alias_to_field(self) -> dict[str, str]:
|
158
|
+
return {
|
159
|
+
field_info.alias: field_name
|
160
|
+
for field_name, field_info in self._get_dialectical_fields().items()
|
161
|
+
}
|
162
|
+
|
163
|
+
def find_component_by_alias(self, alias: str) -> DialecticalComponent | None:
|
164
|
+
"""
|
165
|
+
Helper method to find a dialectical component within a wheel segment by its alias.
|
166
|
+
"""
|
167
|
+
for f, a in self.field_to_alias.items():
|
168
|
+
dc = getattr(self, f)
|
169
|
+
if isinstance(dc, DialecticalComponent):
|
170
|
+
if dc.alias == alias:
|
171
|
+
return dc
|
172
|
+
|
173
|
+
return None
|
174
|
+
|
175
|
+
def add_indexes_to_aliases(self, human_friendly_index: int):
|
176
|
+
"""
|
177
|
+
Updates the aliases of dialectical components with an index to make them unique.
|
178
|
+
|
179
|
+
This method iterates over the field-to-alias mapping of the object and modifies the alias
|
180
|
+
of attributes that are instances of DialecticalComponent. The modified alias includes a
|
181
|
+
human-friendly index appended to its base while preserving any existing sign.
|
182
|
+
|
183
|
+
If the index was there, it will be overwritten.
|
184
|
+
|
185
|
+
Parameters:
|
186
|
+
human_friendly_index: int
|
187
|
+
An index to append to the base of the alias to ensure uniqueness.
|
188
|
+
"""
|
189
|
+
for f, a in self.field_to_alias.items():
|
190
|
+
dc = getattr(self, f)
|
191
|
+
if isinstance(dc, DialecticalComponent):
|
192
|
+
dc.set_human_friendly_index(human_friendly_index)
|
193
|
+
|
194
|
+
def pretty(self) -> str:
|
195
|
+
ws_formatted = []
|
196
|
+
for f, a in self.field_to_alias.items():
|
197
|
+
dc = getattr(self, f)
|
198
|
+
if isinstance(dc, DialecticalComponent):
|
199
|
+
ws_formatted.append(dc.pretty())
|
200
|
+
return "\n\n".join(ws_formatted)
|
201
|
+
|
202
|
+
def __str__(self):
|
203
|
+
return self.pretty()
|