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,419 @@
|
|
1
|
+
from typing import (Callable, Dict, Generic, Tuple, TypeVar, Union, overload)
|
2
|
+
|
3
|
+
from dialectical_framework.analyst.domain.transition import Transition
|
4
|
+
from dialectical_framework.analyst.domain.transition_cell_to_cell import \
|
5
|
+
TransitionCellToCell
|
6
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
7
|
+
from dialectical_framework.enums.predicate import Predicate
|
8
|
+
from dialectical_framework.wheel_segment import WheelSegment
|
9
|
+
|
10
|
+
T = TypeVar("T", bound=Transition)
|
11
|
+
|
12
|
+
AliasInput = Union[str, DialecticalComponent, list[Union[str, DialecticalComponent]]]
|
13
|
+
|
14
|
+
|
15
|
+
def _extract_aliases(input_data: AliasInput) -> list[str]:
|
16
|
+
"""Extract aliases from various input types."""
|
17
|
+
if isinstance(input_data, str):
|
18
|
+
return [input_data]
|
19
|
+
elif isinstance(input_data, DialecticalComponent):
|
20
|
+
return [input_data.alias]
|
21
|
+
elif isinstance(input_data, list):
|
22
|
+
aliases = []
|
23
|
+
for item in input_data:
|
24
|
+
if isinstance(item, str):
|
25
|
+
aliases.append(item)
|
26
|
+
elif isinstance(item, DialecticalComponent):
|
27
|
+
aliases.append(item.alias)
|
28
|
+
return aliases
|
29
|
+
else:
|
30
|
+
raise TypeError(f"Unsupported input type: {type(input_data)}")
|
31
|
+
|
32
|
+
|
33
|
+
class DirectedGraph(Generic[T]):
|
34
|
+
def __init__(self):
|
35
|
+
self._transitions: Dict[Tuple[frozenset[str], frozenset[str]], T] = {}
|
36
|
+
|
37
|
+
def add_transition(self, transition: T) -> None:
|
38
|
+
"""Add a transition to the directed graph."""
|
39
|
+
key = (
|
40
|
+
frozenset(transition.source_aliases),
|
41
|
+
frozenset(transition.target_aliases),
|
42
|
+
)
|
43
|
+
|
44
|
+
# For TransitionOneToOne, ensure only one outgoing transition per source
|
45
|
+
if isinstance(transition, TransitionCellToCell):
|
46
|
+
for existing_key, existing_transition in self._transitions.items():
|
47
|
+
if existing_key[0] == key[0]: # Same source aliases
|
48
|
+
raise ValueError(
|
49
|
+
f"Multiple outgoing transitions for source aliases {transition.source_aliases} found. Only one is allowed."
|
50
|
+
)
|
51
|
+
|
52
|
+
self._transitions[key] = transition
|
53
|
+
|
54
|
+
def get_transition(
|
55
|
+
self, source_aliases: AliasInput, target_aliases: AliasInput
|
56
|
+
) -> T | None:
|
57
|
+
"""Retrieve a transition by source and target aliases."""
|
58
|
+
source_aliases_list = _extract_aliases(source_aliases)
|
59
|
+
target_aliases_list = _extract_aliases(target_aliases)
|
60
|
+
key = (frozenset(source_aliases_list), frozenset(target_aliases_list))
|
61
|
+
return self._transitions.get(key)
|
62
|
+
|
63
|
+
def get_transitions_from_source(self, source_aliases: AliasInput) -> list[T]:
|
64
|
+
"""Get all transitions that have the given source aliases."""
|
65
|
+
source_aliases_list = _extract_aliases(source_aliases)
|
66
|
+
source_set = frozenset(source_aliases_list)
|
67
|
+
return [
|
68
|
+
transition
|
69
|
+
for (src_set, _), transition in self._transitions.items()
|
70
|
+
if src_set == source_set
|
71
|
+
]
|
72
|
+
|
73
|
+
def get_first_transition_from_source(self, source_aliases: AliasInput) -> T | None:
|
74
|
+
"""Get the first transition that has the given source aliases."""
|
75
|
+
transitions = self.get_transitions_from_source(source_aliases)
|
76
|
+
return transitions[0] if transitions else None
|
77
|
+
|
78
|
+
def get_all_transitions(self) -> list[T]:
|
79
|
+
"""
|
80
|
+
Get all transitions in the graph. Be mindful that these transitions might not be forming paths.
|
81
|
+
So rather consider using the "traverse_dfs_with_paths" or "first_path" method.
|
82
|
+
"""
|
83
|
+
return list(self._transitions.values())
|
84
|
+
|
85
|
+
def get_transition_count(self) -> int:
|
86
|
+
"""Get the number of transitions in the graph."""
|
87
|
+
return len(self._transitions)
|
88
|
+
|
89
|
+
def is_empty(self) -> bool:
|
90
|
+
"""Check if the graph has no transitions."""
|
91
|
+
return len(self._transitions) == 0
|
92
|
+
|
93
|
+
def find_outbound_source_aliases(self, start: WheelSegment) -> list[list[str]]:
|
94
|
+
outbound_source_aliases = []
|
95
|
+
start_possible_components = {
|
96
|
+
comp.alias for comp in [start.t, start.t_plus, start.t_minus] if comp
|
97
|
+
}
|
98
|
+
for (_, _), transition in self._transitions.items():
|
99
|
+
transition_source = transition.source
|
100
|
+
if isinstance(transition_source, DialecticalComponent):
|
101
|
+
transition_source_components = {transition_source.alias}
|
102
|
+
else: # WheelSegment
|
103
|
+
transition_source_components = {
|
104
|
+
comp.alias
|
105
|
+
for comp in [
|
106
|
+
transition_source.t,
|
107
|
+
transition_source.t_plus,
|
108
|
+
transition_source.t_minus,
|
109
|
+
]
|
110
|
+
if comp
|
111
|
+
}
|
112
|
+
|
113
|
+
if start_possible_components.issubset(transition_source_components):
|
114
|
+
outbound_source_aliases.append(transition.source_aliases)
|
115
|
+
|
116
|
+
return outbound_source_aliases
|
117
|
+
|
118
|
+
@overload
|
119
|
+
def remove_transition(
|
120
|
+
self, source_aliases: AliasInput, target_aliases: AliasInput
|
121
|
+
) -> bool:
|
122
|
+
"""Remove a transition by source and target aliases. Returns True if removed, False if not found."""
|
123
|
+
...
|
124
|
+
|
125
|
+
@overload
|
126
|
+
def remove_transition(self, transition: T) -> bool:
|
127
|
+
"""Remove a transition by Transition object."""
|
128
|
+
...
|
129
|
+
|
130
|
+
def remove_transition(
|
131
|
+
self, source_aliases_or_transition, target_aliases=None
|
132
|
+
) -> bool:
|
133
|
+
"""Remove a transition by source and target aliases or by Transition object."""
|
134
|
+
if hasattr(source_aliases_or_transition, "source_aliases") and hasattr(
|
135
|
+
source_aliases_or_transition, "target_aliases"
|
136
|
+
):
|
137
|
+
# Called with Transition object
|
138
|
+
transition = source_aliases_or_transition
|
139
|
+
key = (
|
140
|
+
frozenset(transition.source_aliases),
|
141
|
+
frozenset(transition.target_aliases),
|
142
|
+
)
|
143
|
+
else:
|
144
|
+
# Called with source_aliases and target_aliases
|
145
|
+
source_aliases_list = _extract_aliases(source_aliases_or_transition)
|
146
|
+
target_aliases_list = _extract_aliases(target_aliases)
|
147
|
+
key = (frozenset(source_aliases_list), frozenset(target_aliases_list))
|
148
|
+
|
149
|
+
if key in self._transitions:
|
150
|
+
del self._transitions[key]
|
151
|
+
return True
|
152
|
+
return False
|
153
|
+
|
154
|
+
def clear(self) -> None:
|
155
|
+
"""Remove all transitions from the graph."""
|
156
|
+
self._transitions.clear()
|
157
|
+
|
158
|
+
def traverse_dfs_with_paths(
|
159
|
+
self,
|
160
|
+
start_aliases: AliasInput | None = None,
|
161
|
+
visit_callback: Callable[[list[T], bool], None] | None = None,
|
162
|
+
predicate_filter: Predicate | None = None,
|
163
|
+
) -> list[list[T]]:
|
164
|
+
"""
|
165
|
+
Traverse all possible paths in the graph, detecting circles per path.
|
166
|
+
Returns all complete paths found.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
start_aliases: Starting aliases for traversal
|
170
|
+
visit_callback: Optional callback function called for each path
|
171
|
+
predicate_filter: If provided, only traverse transitions with this predicate type
|
172
|
+
"""
|
173
|
+
all_paths = []
|
174
|
+
|
175
|
+
def _can_connect_constructively(
|
176
|
+
target_aliases: list[str], next_source_aliases: list[str]
|
177
|
+
) -> bool:
|
178
|
+
"""
|
179
|
+
For constructively_converges_to, check if segments can connect at the segment level.
|
180
|
+
E.g., [T1-, T1] -> [T2+] can connect to [T2-, T2] -> [T3+] because T2 is shared.
|
181
|
+
"""
|
182
|
+
# Extract base aliases (remove +/- suffixes)
|
183
|
+
target_bases = {alias.rstrip("+-") for alias in target_aliases}
|
184
|
+
source_bases = {alias.rstrip("+-") for alias in next_source_aliases}
|
185
|
+
|
186
|
+
# Check if there's any overlap at the base level
|
187
|
+
return bool(target_bases & source_bases)
|
188
|
+
|
189
|
+
def _find_constructive_transitions(
|
190
|
+
current_target_aliases: list[str],
|
191
|
+
) -> list[T]:
|
192
|
+
"""
|
193
|
+
Find all transitions that can constructively connect to the current target.
|
194
|
+
"""
|
195
|
+
constructive_transitions = []
|
196
|
+
all_transitions_inner = self.get_all_transitions()
|
197
|
+
|
198
|
+
for transition in all_transitions_inner:
|
199
|
+
if (
|
200
|
+
transition.predicate == Predicate.CONSTRUCTIVELY_CONVERGES_TO
|
201
|
+
and _can_connect_constructively(
|
202
|
+
current_target_aliases, transition.source_aliases
|
203
|
+
)
|
204
|
+
):
|
205
|
+
constructive_transitions.append(transition)
|
206
|
+
|
207
|
+
return constructive_transitions
|
208
|
+
|
209
|
+
def _would_create_constructive_cycle(
|
210
|
+
current_aliases: list[str], visited_in_path: set
|
211
|
+
) -> bool:
|
212
|
+
"""
|
213
|
+
For constructively_converges_to, check if current aliases would create a cycle
|
214
|
+
by checking segment-level overlap with any visited node.
|
215
|
+
"""
|
216
|
+
current_bases = {alias.rstrip("+-") for alias in current_aliases}
|
217
|
+
|
218
|
+
for visited_key in visited_in_path:
|
219
|
+
visited_bases = {alias.rstrip("+-") for alias in visited_key}
|
220
|
+
if current_bases & visited_bases: # If there's any base overlap
|
221
|
+
return True
|
222
|
+
return False
|
223
|
+
|
224
|
+
def _dfs_helper(
|
225
|
+
current_aliases: list[str],
|
226
|
+
current_path: list[T],
|
227
|
+
visited_in_path: set,
|
228
|
+
current_predicate: str | None = None,
|
229
|
+
):
|
230
|
+
current_key = frozenset(current_aliases)
|
231
|
+
|
232
|
+
# Check for cycle based on predicate type
|
233
|
+
if current_predicate == Predicate.CONSTRUCTIVELY_CONVERGES_TO:
|
234
|
+
# For constructive convergence, check segment-level overlap
|
235
|
+
if _would_create_constructive_cycle(current_aliases, visited_in_path):
|
236
|
+
# Found a cycle in this path - record the path up to this point
|
237
|
+
all_paths.append(current_path.copy())
|
238
|
+
if visit_callback:
|
239
|
+
visit_callback(current_path, True)
|
240
|
+
return
|
241
|
+
else:
|
242
|
+
# For other predicates, use exact alias matching
|
243
|
+
if current_key in visited_in_path:
|
244
|
+
# Found a cycle in this path - record the path up to this point
|
245
|
+
all_paths.append(current_path.copy())
|
246
|
+
if visit_callback:
|
247
|
+
visit_callback(current_path, True)
|
248
|
+
return
|
249
|
+
|
250
|
+
# Add to current path's visited set
|
251
|
+
new_visited = visited_in_path | {current_key}
|
252
|
+
|
253
|
+
# Get transitions based on predicate type
|
254
|
+
if current_predicate == Predicate.CONSTRUCTIVELY_CONVERGES_TO:
|
255
|
+
# For constructive convergence, find transitions that can connect at segment level
|
256
|
+
transitions = _find_constructive_transitions(current_aliases)
|
257
|
+
else:
|
258
|
+
# Normal case: get transitions from exact source match
|
259
|
+
transitions = self.get_transitions_from_source(current_aliases)
|
260
|
+
|
261
|
+
# Filter transitions based on predicate requirements
|
262
|
+
if predicate_filter is not None:
|
263
|
+
# Only allow transitions with the specified predicate
|
264
|
+
transitions = [
|
265
|
+
t for t in transitions if t.predicate == predicate_filter
|
266
|
+
]
|
267
|
+
elif current_predicate is not None:
|
268
|
+
# If we're already in a path with a specific predicate, only allow same predicate
|
269
|
+
transitions = [
|
270
|
+
t for t in transitions if t.predicate == current_predicate
|
271
|
+
]
|
272
|
+
|
273
|
+
if not transitions:
|
274
|
+
# End of path - record it
|
275
|
+
all_paths.append(current_path.copy())
|
276
|
+
if visit_callback:
|
277
|
+
visit_callback(current_path, False)
|
278
|
+
return
|
279
|
+
|
280
|
+
# Explore each outgoing edge separately
|
281
|
+
for transition in transitions:
|
282
|
+
new_path = current_path + [transition]
|
283
|
+
target_key = frozenset(transition.target_aliases)
|
284
|
+
|
285
|
+
# Determine the predicate for the next step
|
286
|
+
next_predicate = (
|
287
|
+
current_predicate
|
288
|
+
if current_predicate is not None
|
289
|
+
else transition.predicate
|
290
|
+
)
|
291
|
+
|
292
|
+
# Check if target would create a cycle based on predicate type
|
293
|
+
if next_predicate == Predicate.CONSTRUCTIVELY_CONVERGES_TO:
|
294
|
+
# For constructive convergence, check segment-level cycle
|
295
|
+
if _would_create_constructive_cycle(
|
296
|
+
transition.target_aliases, new_visited
|
297
|
+
):
|
298
|
+
# This transition would create a cycle, record the path including this transition
|
299
|
+
all_paths.append(new_path.copy())
|
300
|
+
if visit_callback:
|
301
|
+
visit_callback(new_path, True)
|
302
|
+
else:
|
303
|
+
# Normal case: continue DFS
|
304
|
+
_dfs_helper(
|
305
|
+
transition.target_aliases,
|
306
|
+
new_path,
|
307
|
+
new_visited,
|
308
|
+
next_predicate,
|
309
|
+
)
|
310
|
+
else:
|
311
|
+
# For other predicates, use exact matching
|
312
|
+
if target_key in new_visited:
|
313
|
+
# This transition would create a cycle, record the path including this transition
|
314
|
+
all_paths.append(new_path.copy())
|
315
|
+
if visit_callback:
|
316
|
+
visit_callback(new_path, True)
|
317
|
+
else:
|
318
|
+
# Normal case: continue DFS
|
319
|
+
_dfs_helper(
|
320
|
+
transition.target_aliases,
|
321
|
+
new_path,
|
322
|
+
new_visited,
|
323
|
+
next_predicate,
|
324
|
+
)
|
325
|
+
|
326
|
+
if start_aliases is None:
|
327
|
+
# Get all transitions and find unique source aliases
|
328
|
+
all_transitions = self.get_all_transitions()
|
329
|
+
if all_transitions:
|
330
|
+
first_transition = all_transitions[0]
|
331
|
+
start_aliases_list = first_transition.source_aliases
|
332
|
+
else:
|
333
|
+
return []
|
334
|
+
else:
|
335
|
+
start_aliases_list = _extract_aliases(start_aliases)
|
336
|
+
|
337
|
+
_dfs_helper(start_aliases_list, [], set())
|
338
|
+
|
339
|
+
return all_paths
|
340
|
+
|
341
|
+
def first_path(self, start_aliases: AliasInput | None = None) -> list[T]:
|
342
|
+
"""
|
343
|
+
We normally deal with cycles and spirals, so first path is mostly enough.
|
344
|
+
"""
|
345
|
+
# TODO: very inefficient, we shouldn't be traversing the whole graph just to get the first path
|
346
|
+
paths = self.traverse_dfs_with_paths(start_aliases)
|
347
|
+
return paths[0] if paths else []
|
348
|
+
|
349
|
+
def count_circles(self) -> int:
|
350
|
+
"""
|
351
|
+
Count the number of distinct cycles (circles) in the directed graph.
|
352
|
+
Each unique cycle is counted only once, regardless of starting point.
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
Number of distinct cycles found in the graph
|
356
|
+
"""
|
357
|
+
if self.is_empty():
|
358
|
+
return 0
|
359
|
+
|
360
|
+
detected_cycles = set()
|
361
|
+
|
362
|
+
# Get all unique starting nodes
|
363
|
+
all_nodes = set()
|
364
|
+
for (src_set, tgt_set), _ in self._transitions.items():
|
365
|
+
all_nodes.update(src_set)
|
366
|
+
all_nodes.update(tgt_set)
|
367
|
+
|
368
|
+
# Check each node as a potential cycle start
|
369
|
+
for start_node in all_nodes:
|
370
|
+
paths = self.traverse_dfs_with_paths(start_node)
|
371
|
+
|
372
|
+
for path in paths:
|
373
|
+
if path: # Non-empty path
|
374
|
+
# Check if end connects back to start
|
375
|
+
last_transition = path[-1]
|
376
|
+
path_end = frozenset(last_transition.target_aliases)
|
377
|
+
path_start = frozenset([start_node])
|
378
|
+
|
379
|
+
if path_end == path_start:
|
380
|
+
# Found a cycle! Extract all nodes in the cycle
|
381
|
+
cycle_nodes = [
|
382
|
+
frozenset([start_node])
|
383
|
+
] # Start with the starting node
|
384
|
+
for transition in path:
|
385
|
+
cycle_nodes.append(frozenset(transition.target_aliases))
|
386
|
+
|
387
|
+
# Remove the duplicate end node (it's the same as start)
|
388
|
+
cycle_nodes = cycle_nodes[:-1]
|
389
|
+
|
390
|
+
# Normalize: start from lexicographically smallest node
|
391
|
+
min_node = min(cycle_nodes)
|
392
|
+
min_idx = cycle_nodes.index(min_node)
|
393
|
+
normalized_cycle = tuple(
|
394
|
+
cycle_nodes[min_idx:] + cycle_nodes[:min_idx]
|
395
|
+
)
|
396
|
+
|
397
|
+
detected_cycles.add(normalized_cycle)
|
398
|
+
|
399
|
+
return len(detected_cycles)
|
400
|
+
|
401
|
+
def pretty(self, start_aliases: AliasInput | None = None) -> str:
|
402
|
+
paths = self.traverse_dfs_with_paths(start_aliases=start_aliases)
|
403
|
+
paths_pieces = []
|
404
|
+
for path in paths:
|
405
|
+
current_str = None
|
406
|
+
for i, transition in enumerate(path):
|
407
|
+
if current_str is None:
|
408
|
+
current_str = f"{transition.source_aliases}"
|
409
|
+
if i == len(path) - 1 and tuple(transition.target_aliases) == tuple(
|
410
|
+
path[0].source_aliases
|
411
|
+
):
|
412
|
+
# Skip last node if it equals the first node (completing the cycle)
|
413
|
+
continue
|
414
|
+
current_str += f" -> {transition.target_aliases}"
|
415
|
+
paths_pieces.append(current_str)
|
416
|
+
return "\n".join(paths_pieces) if paths_pieces else "<empty>"
|
417
|
+
|
418
|
+
def __str__(self):
|
419
|
+
return self.pretty()
|
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
|
4
|
+
class DI(str, Enum):
|
5
|
+
"""Dependency injection provider names, for easier refactoring"""
|
6
|
+
|
7
|
+
settings = "settings"
|
8
|
+
brain = "brain"
|
9
|
+
polarity_reasoner = "polarity_reasoner"
|
10
|
+
causality_sequencer = "causality_sequencer"
|
11
|
+
thesis_extractor = "thesis_extractor"
|
File without changes
|
@@ -0,0 +1,141 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from typing import TYPE_CHECKING, final, TypeVar
|
5
|
+
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
7
|
+
|
8
|
+
from dialectical_framework.utils.gm import gm_with_zeros_and_nones_handled
|
9
|
+
|
10
|
+
if TYPE_CHECKING: # Conditionally import Rationale for type checking only
|
11
|
+
from dialectical_framework.analyst.domain.rationale import Rationale
|
12
|
+
|
13
|
+
class Assessable(BaseModel, ABC):
|
14
|
+
model_config = ConfigDict(
|
15
|
+
arbitrary_types_allowed=True,
|
16
|
+
)
|
17
|
+
|
18
|
+
score: float | None = Field(
|
19
|
+
default=None,
|
20
|
+
ge=0.0, le=1.0,
|
21
|
+
description="The final composite score (Pr(S) * CF_S^alpha) for ranking."
|
22
|
+
)
|
23
|
+
|
24
|
+
contextual_fidelity: float | None = Field(default=None, description="Grounding in the initial context")
|
25
|
+
|
26
|
+
probability: float | None = Field(
|
27
|
+
default=None,
|
28
|
+
ge=0.0, le=1.0,
|
29
|
+
description="The normalized probability (Pr(S)) of the cycle to exist in reality.",
|
30
|
+
)
|
31
|
+
|
32
|
+
rationales: list[Rationale] = Field(default_factory=list, description="Reasoning about this assessable instance")
|
33
|
+
|
34
|
+
@property
|
35
|
+
def best_rationale(self) -> Rationale | None:
|
36
|
+
if self.rationales and len(self.rationales) > 1:
|
37
|
+
return self.rationales[0]
|
38
|
+
|
39
|
+
selected_r = None
|
40
|
+
best_score = None # use None sentinel
|
41
|
+
|
42
|
+
for r in self.rationales or []:
|
43
|
+
r_score = r.calculate_score(mutate=False)
|
44
|
+
if r_score is not None and (best_score is None or r_score > best_score):
|
45
|
+
best_score = r_score
|
46
|
+
selected_r = r
|
47
|
+
|
48
|
+
if selected_r is not None:
|
49
|
+
return selected_r
|
50
|
+
# fallback: first rationale if present
|
51
|
+
return self.rationales[0] if self.rationales else None
|
52
|
+
|
53
|
+
@final
|
54
|
+
def calculate_score(self, *, alpha: float = 1.0, mutate: bool = True) -> float | None:
|
55
|
+
"""
|
56
|
+
Calculates composite score: Score(X) = Pr(S) × CF_X^α
|
57
|
+
|
58
|
+
Two-layer weighting system:
|
59
|
+
- rating: Domain expert weighting (applied during aggregation)
|
60
|
+
- alpha: System-level parameter for contextual fidelity importance
|
61
|
+
|
62
|
+
Args:
|
63
|
+
alpha: Contextual fidelity exponent
|
64
|
+
< 1.0: De-emphasize expert context assessments
|
65
|
+
= 1.0: Respect expert ratings fully (default)
|
66
|
+
> 1.0: Amplify expert context assessments
|
67
|
+
"""
|
68
|
+
# First, recursively calculate scores for all sub-assessables
|
69
|
+
sub_assessables = self._get_sub_assessables()
|
70
|
+
for sub_assessable in sub_assessables:
|
71
|
+
sub_assessable.calculate_score(alpha=alpha, mutate=mutate)
|
72
|
+
|
73
|
+
# Ensure that the overall probability has been calculated
|
74
|
+
probability = self.calculate_probability(mutate=mutate)
|
75
|
+
# Always calculate contextual fidelity, even if probability is None
|
76
|
+
cf_w = self.calculate_contextual_fidelity(mutate=mutate)
|
77
|
+
|
78
|
+
if probability is None:
|
79
|
+
# If still None, cannot calculate score
|
80
|
+
score = None
|
81
|
+
else:
|
82
|
+
score = probability * (cf_w ** alpha)
|
83
|
+
|
84
|
+
if mutate:
|
85
|
+
self.score = score
|
86
|
+
|
87
|
+
return self.score
|
88
|
+
|
89
|
+
def calculate_contextual_fidelity(self, *, mutate: bool = True) -> float:
|
90
|
+
"""
|
91
|
+
If not possible to calculate contextual fidelity, return 1.0 to have neutral impact on overall scoring.
|
92
|
+
|
93
|
+
Normally this method shouldn't be called, as it's called by the `calculate_score` method.
|
94
|
+
"""
|
95
|
+
all_fidelities = []
|
96
|
+
all_fidelities.extend(self._calculate_contextual_fidelity_for_rationales())
|
97
|
+
all_fidelities.extend(self._calculate_contextual_fidelity_for_sub_elements_excl_rationales())
|
98
|
+
|
99
|
+
if not all_fidelities:
|
100
|
+
fidelity = 1.0 # Neutral impact if no components with positive scores
|
101
|
+
else:
|
102
|
+
fidelity = gm_with_zeros_and_nones_handled(all_fidelities)
|
103
|
+
|
104
|
+
if mutate:
|
105
|
+
self.contextual_fidelity = fidelity
|
106
|
+
|
107
|
+
return fidelity
|
108
|
+
|
109
|
+
@abstractmethod
|
110
|
+
def calculate_probability(self, *, mutate: bool = True) -> float | None: ...
|
111
|
+
"""
|
112
|
+
Normally this method shouldn't be called, as it's called by the `calculate_score` method.
|
113
|
+
"""
|
114
|
+
|
115
|
+
def _get_sub_assessables(self) -> list[Assessable]:
|
116
|
+
"""
|
117
|
+
Returns all direct sub-assessable elements contained within this assessable.
|
118
|
+
Used for recursive score calculation.
|
119
|
+
"""
|
120
|
+
# IMPORTANT: we must work on a copy, to avoid filling rationales list with garbage
|
121
|
+
return [*self.rationales]
|
122
|
+
|
123
|
+
def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
|
124
|
+
return []
|
125
|
+
|
126
|
+
@final
|
127
|
+
def _calculate_contextual_fidelity_for_rationales(self, *, mutate: bool = True) -> list[float]:
|
128
|
+
fids: list[float] = []
|
129
|
+
if self.rationales:
|
130
|
+
for rationale in self.rationales:
|
131
|
+
# IMPORTANT: use the evidence view to avoid 1.0 fallback inflation
|
132
|
+
evidence_cf = rationale.calculate_contextual_fidelity_evidence(mutate=mutate)
|
133
|
+
|
134
|
+
if evidence_cf is not None and evidence_cf > 0.0:
|
135
|
+
weighted = evidence_cf * rationale.rating_or_default()
|
136
|
+
if weighted > 0.0: # skip non-positives after weighting
|
137
|
+
fids.append(weighted)
|
138
|
+
return fids
|
139
|
+
|
140
|
+
def _calculate_probability_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
|
141
|
+
return []
|