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,116 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional, List
|
4
|
+
|
5
|
+
from pydantic import Field
|
6
|
+
|
7
|
+
from dialectical_framework.protocols.assessable import Assessable
|
8
|
+
from dialectical_framework.protocols.ratable import Ratable
|
9
|
+
from dialectical_framework.utils.gm import gm_with_zeros_and_nones_handled
|
10
|
+
from dialectical_framework.wheel import Wheel
|
11
|
+
|
12
|
+
class Rationale(Ratable):
|
13
|
+
headline: Optional[str] = Field(default=None)
|
14
|
+
summary: Optional[str] = Field(default=None)
|
15
|
+
text: Optional[str] = Field(default=None)
|
16
|
+
theses: list[str] = Field(default_factory=list, description="Theses of the rationale text.")
|
17
|
+
wheels: list[Wheel] = Field(default_factory=list, description="Wheels that are digging deeper into the rationale.")
|
18
|
+
|
19
|
+
def _hard_veto_on_own_zero(self) -> bool:
|
20
|
+
"""
|
21
|
+
Why not veto Rationale by default
|
22
|
+
|
23
|
+
Rationale is commentary/evidence, not structure. It can be refuted by critiques or outweighed by spawned wheels. One mistaken rationale with CF=0 shouldn’t nuke the parent.
|
24
|
+
|
25
|
+
You already have a safe “off” switch: set rationale.rating = 0 → its contribution is ignored without collapsing CF to 0.
|
26
|
+
|
27
|
+
True veto belongs at structural leaves (Components, Transitions), where “this is contextually impossible” should indeed zero things.
|
28
|
+
"""
|
29
|
+
return False
|
30
|
+
|
31
|
+
def _get_sub_assessables(self) -> list[Assessable]:
|
32
|
+
result = super()._get_sub_assessables()
|
33
|
+
result.extend(self.wheels)
|
34
|
+
return result
|
35
|
+
|
36
|
+
def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
|
37
|
+
"""
|
38
|
+
CF(rationale) is evidence-driven:
|
39
|
+
- If there is child evidence (wheels/critiques), aggregate it.
|
40
|
+
- Otherwise, fall back to the rationale's own CF × rating (via get_fidelity()).
|
41
|
+
Do NOT apply self.get_rating() to child wheels; the parent that consumes this
|
42
|
+
rationale will apply rationale.rating to CF(rationale).
|
43
|
+
"""
|
44
|
+
parts: list[float] = []
|
45
|
+
|
46
|
+
# Wheels spawned by this rationale — include as-is (no rating here)
|
47
|
+
for wheel in self.wheels:
|
48
|
+
w_cf = wheel.calculate_contextual_fidelity(mutate=mutate)
|
49
|
+
if w_cf is not None and w_cf > 0.0:
|
50
|
+
parts.append(w_cf)
|
51
|
+
|
52
|
+
return parts
|
53
|
+
|
54
|
+
def calculate_contextual_fidelity_evidence(self, *, mutate: bool = True) -> float | None:
|
55
|
+
parts: List[float] = []
|
56
|
+
|
57
|
+
# Critiques (child rationales) — recurse using EVIDENCE path
|
58
|
+
if self.rationales:
|
59
|
+
for r in self.rationales:
|
60
|
+
cf = r.calculate_contextual_fidelity_evidence(mutate=mutate)
|
61
|
+
if cf is not None and cf > 0.0:
|
62
|
+
parts.append(cf)
|
63
|
+
|
64
|
+
# Wheels spawned by this rationale (unrated here)
|
65
|
+
for w in self.wheels:
|
66
|
+
cf = w.calculate_contextual_fidelity(mutate=mutate)
|
67
|
+
if cf is not None and cf > 0.0:
|
68
|
+
parts.append(cf)
|
69
|
+
|
70
|
+
# Own intrinsic CF (manual only), unweighted here
|
71
|
+
own_cf = self.contextual_fidelity
|
72
|
+
if own_cf is not None:
|
73
|
+
if own_cf == 0.0 and self._hard_veto_on_own_zero():
|
74
|
+
return 0.0 # explicit veto only if you enabled it for rationales
|
75
|
+
if own_cf > 0.0:
|
76
|
+
parts.append(own_cf)
|
77
|
+
|
78
|
+
# If no real CF signal, return None so parent SKIPS this rationale
|
79
|
+
return gm_with_zeros_and_nones_handled(parts) if parts else None
|
80
|
+
|
81
|
+
def calculate_contextual_fidelity(self, *, mutate: bool = True) -> float:
|
82
|
+
# ---- SELF-SCORING PATH (for the rationale itself). Allows neutral fallback = 1.0. ----
|
83
|
+
cf_e = self.calculate_contextual_fidelity_evidence(mutate=mutate)
|
84
|
+
fidelity = 1.0 if cf_e is None else cf_e
|
85
|
+
|
86
|
+
# IMPORTANT: do NOT write derived CF back into contextual_fidelity
|
87
|
+
# That field remains manual-only; writing here would be mistaken as evidence later.
|
88
|
+
return fidelity
|
89
|
+
|
90
|
+
def calculate_probability_evidence(self, *, mutate: bool = True) -> float | None:
|
91
|
+
parts: list[float] = []
|
92
|
+
|
93
|
+
# 1) Wheels spawned by this rationale
|
94
|
+
for wheel in self.wheels:
|
95
|
+
p = wheel.calculate_probability(mutate=mutate)
|
96
|
+
if p is not None:
|
97
|
+
parts.append(p)
|
98
|
+
|
99
|
+
# 2) Critiques as rationales (include their full probability, not just their wheels)
|
100
|
+
for critique in self.rationales:
|
101
|
+
p = critique.calculate_probability_evidence(mutate=mutate)
|
102
|
+
if p is not None:
|
103
|
+
parts.append(p)
|
104
|
+
|
105
|
+
# Child-first: if children exist, use them
|
106
|
+
return gm_with_zeros_and_nones_handled(parts) if parts else None
|
107
|
+
|
108
|
+
def calculate_probability(self, *, mutate: bool = True) -> float | None:
|
109
|
+
probability = self.calculate_probability_evidence(mutate=True)
|
110
|
+
|
111
|
+
if probability is None:
|
112
|
+
return 1.0
|
113
|
+
|
114
|
+
if mutate:
|
115
|
+
self.probability = probability
|
116
|
+
return probability
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
from pydantic import ConfigDict
|
3
|
+
|
4
|
+
from dialectical_framework.analyst.domain.assessable_cycle import \
|
5
|
+
AssessableCycle
|
6
|
+
from dialectical_framework.analyst.domain.transition_segment_to_segment import \
|
7
|
+
TransitionSegmentToSegment
|
8
|
+
from dialectical_framework.directed_graph import DirectedGraph
|
9
|
+
from dialectical_framework.wheel_segment import WheelSegment
|
10
|
+
|
11
|
+
|
12
|
+
class Spiral(AssessableCycle):
|
13
|
+
model_config = ConfigDict(
|
14
|
+
extra="forbid",
|
15
|
+
arbitrary_types_allowed=True,
|
16
|
+
)
|
17
|
+
|
18
|
+
def __init__(self, graph: DirectedGraph[TransitionSegmentToSegment] = None, **data):
|
19
|
+
super().__init__(**data)
|
20
|
+
if self.graph is None:
|
21
|
+
self.graph = (
|
22
|
+
graph
|
23
|
+
if graph is not None
|
24
|
+
else DirectedGraph[TransitionSegmentToSegment]()
|
25
|
+
)
|
26
|
+
|
27
|
+
def pretty(self, *, start_wheel_segment: WheelSegment) -> str:
|
28
|
+
output = []
|
29
|
+
|
30
|
+
source_aliases_list = self.graph.find_outbound_source_aliases(
|
31
|
+
start=start_wheel_segment
|
32
|
+
)
|
33
|
+
for source_aliases in source_aliases_list:
|
34
|
+
output.append(self.graph.pretty(start_aliases=source_aliases))
|
35
|
+
path = self.graph.first_path(start_aliases=source_aliases)
|
36
|
+
if path:
|
37
|
+
for transition in path:
|
38
|
+
output.append(str(transition))
|
39
|
+
else:
|
40
|
+
raise ValueError(f"No path found from {source_aliases}.")
|
41
|
+
|
42
|
+
return "\n".join(output)
|
43
|
+
|
44
|
+
def __str__(self):
|
45
|
+
return self.pretty(
|
46
|
+
start_wheel_segment=self.graph.get_all_transitions()[0].source
|
47
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import ConfigDict, Field
|
4
|
+
|
5
|
+
from dialectical_framework.protocols.assessable import Assessable
|
6
|
+
from dialectical_framework.analyst.domain.spiral import Spiral
|
7
|
+
from dialectical_framework.wisdom_unit import WisdomUnit
|
8
|
+
|
9
|
+
|
10
|
+
class Transformation(Spiral):
|
11
|
+
model_config = ConfigDict(
|
12
|
+
extra="forbid",
|
13
|
+
arbitrary_types_allowed=True,
|
14
|
+
)
|
15
|
+
|
16
|
+
ac_re: WisdomUnit = Field(..., description="Action-reflection wisdom unit")
|
17
|
+
|
18
|
+
def _get_sub_assessables(self) -> list[Assessable]:
|
19
|
+
result = super()._get_sub_assessables()
|
20
|
+
result.append(self.ac_re)
|
21
|
+
return result
|
22
|
+
|
23
|
+
def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
|
24
|
+
return [self.ac_re.calculate_contextual_fidelity(mutate=mutate)]
|
@@ -0,0 +1,160 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Tuple, Union
|
4
|
+
|
5
|
+
from pydantic import ConfigDict, Field, field_validator
|
6
|
+
|
7
|
+
from dialectical_framework.protocols.assessable import Assessable
|
8
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
9
|
+
from dialectical_framework.enums.predicate import Predicate
|
10
|
+
from dialectical_framework.protocols.ratable import Ratable
|
11
|
+
from dialectical_framework.utils.gm import gm_with_zeros_and_nones_handled
|
12
|
+
from dialectical_framework.wheel_segment import WheelSegment
|
13
|
+
|
14
|
+
|
15
|
+
class Transition(Ratable):
|
16
|
+
model_config = ConfigDict(
|
17
|
+
extra="forbid",
|
18
|
+
)
|
19
|
+
|
20
|
+
source_aliases: list[str] = Field(
|
21
|
+
default_factory=list, description="Aliases of the source segment of the wheel."
|
22
|
+
)
|
23
|
+
source: Union[WheelSegment, DialecticalComponent] = Field(
|
24
|
+
description="Source segment of the wheel or dialectical component."
|
25
|
+
)
|
26
|
+
|
27
|
+
target_aliases: list[str] = Field(
|
28
|
+
default_factory=list, description="Aliases of the target segment of the wheel."
|
29
|
+
)
|
30
|
+
target: Union[WheelSegment, DialecticalComponent] = Field(
|
31
|
+
description="Target segment of the wheel or dialectical component."
|
32
|
+
)
|
33
|
+
|
34
|
+
predicate: Predicate = Field(
|
35
|
+
...,
|
36
|
+
description="The type of relationship between the source and target, e.g. T1 => causes => T2.",
|
37
|
+
)
|
38
|
+
|
39
|
+
manual_probability: float | None = Field(
|
40
|
+
default=None,
|
41
|
+
ge=0.0, le=1.0,
|
42
|
+
description="The normalized probability (Pr(S)) of the cycle to exist in reality.",
|
43
|
+
)
|
44
|
+
|
45
|
+
def _get_sub_assessables(self) -> list[Assessable]:
|
46
|
+
"""
|
47
|
+
Don't add source/target here, as these are part of the wheel, and we'll end up infinitely recursing.
|
48
|
+
"""
|
49
|
+
return super()._get_sub_assessables()
|
50
|
+
|
51
|
+
def get_key(self) -> Tuple[frozenset[str], frozenset[str]]:
|
52
|
+
"""Get the key used to uniquely identify this transition based on source and target aliases."""
|
53
|
+
return (
|
54
|
+
frozenset(self.source_aliases),
|
55
|
+
frozenset(self.target_aliases),
|
56
|
+
)
|
57
|
+
|
58
|
+
@property
|
59
|
+
def advice(self) -> str | None:
|
60
|
+
r = self.best_rationale
|
61
|
+
if r:
|
62
|
+
return r.text
|
63
|
+
else:
|
64
|
+
return None
|
65
|
+
|
66
|
+
def calculate_probability(self, *, mutate: bool = True) -> float | None:
|
67
|
+
parts: list[float] = []
|
68
|
+
|
69
|
+
# Own manual probability (optional) × own confidence
|
70
|
+
p_self = self.manual_probability
|
71
|
+
if p_self is not None:
|
72
|
+
w_self = self.confidence_or_default()
|
73
|
+
if w_self > 0.0:
|
74
|
+
# include 0.0 if p_self==0 and w_self>0 (explicit veto is allowed at the leaf)
|
75
|
+
parts.append(p_self * w_self)
|
76
|
+
|
77
|
+
# Rationale probabilities × rationale confidence
|
78
|
+
for rationale in (self.rationales or []):
|
79
|
+
# NOTE: We rely on the evidence, not on the fallback value, that's important
|
80
|
+
p = rationale.calculate_probability_evidence(mutate=mutate)
|
81
|
+
if p is None:
|
82
|
+
continue
|
83
|
+
w = rationale.confidence_or_default()
|
84
|
+
v = p * w
|
85
|
+
if v > 0.0: # skip non-positive after weighting
|
86
|
+
parts.append(v)
|
87
|
+
|
88
|
+
probability = gm_with_zeros_and_nones_handled(parts) if parts else None
|
89
|
+
|
90
|
+
if mutate:
|
91
|
+
self.probability = probability
|
92
|
+
return probability
|
93
|
+
|
94
|
+
@field_validator("source_aliases")
|
95
|
+
def validate_source_aliases(cls, v: list[str], info) -> list[str]: # Change list[str] from list[str]
|
96
|
+
if "source" in info.data and info.data["source"]:
|
97
|
+
source = info.data["source"]
|
98
|
+
valid_aliases = []
|
99
|
+
|
100
|
+
if isinstance(source, DialecticalComponent):
|
101
|
+
valid_aliases = [source.alias]
|
102
|
+
elif isinstance(source, WheelSegment):
|
103
|
+
# Extract aliases from all non-None components in the WheelSegment
|
104
|
+
for component in [source.t, source.t_plus, source.t_minus]:
|
105
|
+
if component:
|
106
|
+
valid_aliases.append(component.alias)
|
107
|
+
|
108
|
+
invalid_aliases = [alias for alias in v if alias not in valid_aliases]
|
109
|
+
if invalid_aliases:
|
110
|
+
raise ValueError(
|
111
|
+
f"Invalid source aliases: {invalid_aliases}. Valid aliases: {valid_aliases}"
|
112
|
+
)
|
113
|
+
return v
|
114
|
+
|
115
|
+
@field_validator("target_aliases")
|
116
|
+
def validate_target_aliases(cls, v: list[str], info) -> list[str]: # Change list[str] from list[str]
|
117
|
+
if "target" in info.data and info.data["target"]:
|
118
|
+
target = info.data["target"]
|
119
|
+
valid_aliases = []
|
120
|
+
|
121
|
+
if isinstance(target, DialecticalComponent):
|
122
|
+
valid_aliases = [target.alias]
|
123
|
+
elif isinstance(target, WheelSegment):
|
124
|
+
# Extract aliases from all non-None components in the WheelSegment
|
125
|
+
for component in [target.t, target.t_plus, target.t_minus]:
|
126
|
+
if component:
|
127
|
+
valid_aliases.append(component.alias)
|
128
|
+
|
129
|
+
invalid_aliases = [alias for alias in v if alias not in valid_aliases]
|
130
|
+
if invalid_aliases:
|
131
|
+
raise ValueError(
|
132
|
+
f"Invalid target aliases: {invalid_aliases}. Valid aliases: {valid_aliases}"
|
133
|
+
)
|
134
|
+
return v
|
135
|
+
|
136
|
+
def new_with(self, other: Transition) -> Transition:
|
137
|
+
self_dict = self.model_dump()
|
138
|
+
other_dict = other.model_dump()
|
139
|
+
|
140
|
+
merged_dict = {**other_dict} # Start with other values
|
141
|
+
# Override with self values that are not None
|
142
|
+
for key, value in self_dict.items():
|
143
|
+
if value is not None:
|
144
|
+
merged_dict[key] = value
|
145
|
+
|
146
|
+
new_t_class: type[Transition] = type(self)
|
147
|
+
if not isinstance(other, Transition):
|
148
|
+
new_t_class = type(other)
|
149
|
+
|
150
|
+
return new_t_class(**merged_dict)
|
151
|
+
|
152
|
+
def pretty(self) -> str:
|
153
|
+
str_pieces = [
|
154
|
+
f"{', '.join(self.source_aliases)} → {', '.join(self.target_aliases)}",
|
155
|
+
f"Summary: {self.advice if self.advice else 'N/A'}",
|
156
|
+
]
|
157
|
+
return "\n".join(str_pieces)
|
158
|
+
|
159
|
+
def __str__(self):
|
160
|
+
return self.pretty()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Self
|
4
|
+
|
5
|
+
from pydantic import Field, model_validator
|
6
|
+
|
7
|
+
from dialectical_framework.analyst.domain.transition import Transition
|
8
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
9
|
+
|
10
|
+
|
11
|
+
class TransitionCellToCell(Transition):
|
12
|
+
source: DialecticalComponent = Field(
|
13
|
+
description="Source dialectical component of the wheel."
|
14
|
+
)
|
15
|
+
target: DialecticalComponent = Field(
|
16
|
+
description="Target dialectical component of the wheel."
|
17
|
+
)
|
18
|
+
|
19
|
+
@model_validator(mode="after")
|
20
|
+
def auto_populate_aliases(self) -> Self:
|
21
|
+
# Autopopulate source_aliases if empty
|
22
|
+
if not self.source_aliases and self.source:
|
23
|
+
self.source_aliases = [self.source.alias]
|
24
|
+
|
25
|
+
# Autopopulate target_aliases if empty
|
26
|
+
if not self.target_aliases and self.target:
|
27
|
+
self.target_aliases = [self.target.alias]
|
28
|
+
|
29
|
+
return self
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from dialectical_framework.analyst.domain.transition import Transition
|
6
|
+
from dialectical_framework.wheel_segment import WheelSegment
|
7
|
+
|
8
|
+
|
9
|
+
class TransitionSegmentToSegment(Transition):
|
10
|
+
"""
|
11
|
+
Note that though the transition is from segment to segment,
|
12
|
+
the source aliases and target aliases can be subsets of segments components.
|
13
|
+
"""
|
14
|
+
|
15
|
+
source: WheelSegment = Field(description="Source segment of the wheel.")
|
16
|
+
target: WheelSegment = Field(description="Target segment of the wheel.")
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Optional, List
|
3
|
+
|
4
|
+
from dialectical_framework.analyst.domain.transition import Transition
|
5
|
+
from dialectical_framework.brain import Brain
|
6
|
+
from dialectical_framework.protocols.has_brain import HasBrain
|
7
|
+
from dialectical_framework.wheel import Wheel
|
8
|
+
from dialectical_framework.wheel_segment import WheelSegment
|
9
|
+
|
10
|
+
|
11
|
+
class StrategicConsultant(ABC, HasBrain):
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
*,
|
15
|
+
text: str,
|
16
|
+
wheel: Wheel,
|
17
|
+
brain: Optional[Brain] = None,
|
18
|
+
):
|
19
|
+
self._text = text
|
20
|
+
self._wheel = wheel
|
21
|
+
self._brain = brain
|
22
|
+
|
23
|
+
@property
|
24
|
+
def brain(self) -> Brain:
|
25
|
+
return super().brain if self._brain is None else self._brain
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
async def think(self, focus: WheelSegment) -> Transition | List[Transition]: ...
|
29
|
+
"""
|
30
|
+
The main method of the class. It should return a Transition to the next WisdomUnit.
|
31
|
+
This Transition must be saved into the current instance.
|
32
|
+
"""
|
@@ -0,0 +1,217 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from mirascope import Messages, prompt_template
|
5
|
+
from mirascope.integrations.langfuse import with_langfuse
|
6
|
+
|
7
|
+
from dialectical_framework import TransitionSegmentToSegment, Transformation, Rationale
|
8
|
+
from dialectical_framework.ai_dto.dialectical_components_deck_dto import \
|
9
|
+
DialecticalComponentsDeckDto
|
10
|
+
from dialectical_framework.ai_dto.dto_mapper import map_list_from_dto
|
11
|
+
from dialectical_framework.ai_dto.reciprocal_solution_dto import ReciprocalSolutionDto
|
12
|
+
from dialectical_framework.analyst.strategic_consultant import \
|
13
|
+
StrategicConsultant
|
14
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
15
|
+
from dialectical_framework.directed_graph import DirectedGraph
|
16
|
+
from dialectical_framework.enums.dialectical_reasoning_mode import \
|
17
|
+
DialecticalReasoningMode
|
18
|
+
from dialectical_framework.enums.predicate import Predicate
|
19
|
+
from dialectical_framework.protocols.has_config import SettingsAware
|
20
|
+
from dialectical_framework.utils.use_brain import use_brain
|
21
|
+
from dialectical_framework.wheel_segment import (ALIAS_T, ALIAS_T_MINUS,
|
22
|
+
ALIAS_T_PLUS, WheelSegment)
|
23
|
+
from dialectical_framework.wisdom_unit import (ALIAS_A, ALIAS_A_MINUS,
|
24
|
+
ALIAS_A_PLUS, WisdomUnit)
|
25
|
+
|
26
|
+
|
27
|
+
ALIAS_AC = "Ac"
|
28
|
+
ALIAS_AC_PLUS = "Ac+"
|
29
|
+
ALIAS_AC_MINUS = "Ac-"
|
30
|
+
ALIAS_RE = "Re"
|
31
|
+
ALIAS_RE_PLUS = "Re+"
|
32
|
+
ALIAS_RE_MINUS = "Re-"
|
33
|
+
|
34
|
+
class ThinkActionReflection(StrategicConsultant, SettingsAware):
|
35
|
+
@prompt_template(
|
36
|
+
"""
|
37
|
+
USER:
|
38
|
+
<context>{text}</context>
|
39
|
+
|
40
|
+
USER:
|
41
|
+
Previous Dialectical Analysis:
|
42
|
+
{dialectical_analysis}
|
43
|
+
|
44
|
+
USER:
|
45
|
+
<instructions>
|
46
|
+
Given the initial context and the previous dialectical analysis, identify the transition steps Ac and Re that transform T and A into each other as follows:
|
47
|
+
1) Ac must transform T into A
|
48
|
+
2) Ac+ must transform T- and/or T into A+
|
49
|
+
3) Ac- must transform T+ and/or T into A-
|
50
|
+
4) Re must transform A into T
|
51
|
+
5) Re+ must transform A- and/or A into T+
|
52
|
+
6) Re- must transform A+ and/or A into T-
|
53
|
+
7) Re+ must oppose/contradict Ac-
|
54
|
+
8) Re- must oppose/contradict Ac+
|
55
|
+
</instructions>
|
56
|
+
|
57
|
+
<formatting>
|
58
|
+
Output each transition step within {component_length} word(s), the shorter, the better. Compose the explanations how they were derived in the passive voice. Don't mention any special denotations such as "T", "T+", "A-", "Ac", "Re", etc.
|
59
|
+
</formatting>
|
60
|
+
"""
|
61
|
+
)
|
62
|
+
def ac_re_prompt(self, text: str, focus: WisdomUnit) -> Messages.Type:
|
63
|
+
# TODO: do we want to include the whole wheel reengineered? Also transitions so far?
|
64
|
+
return {
|
65
|
+
"computed_fields": {
|
66
|
+
"text": text,
|
67
|
+
"dialectical_analysis": focus.pretty(),
|
68
|
+
"component_length": self.settings.component_length,
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
@prompt_template(
|
73
|
+
"""
|
74
|
+
USER:
|
75
|
+
<context>{text}</context>
|
76
|
+
|
77
|
+
USER:
|
78
|
+
Previous Dialectical Analysis:
|
79
|
+
{dialectical_analysis}
|
80
|
+
|
81
|
+
USER:
|
82
|
+
<instructions>
|
83
|
+
Given the initial context and the previous dialectical analysis, suggest solution(s).
|
84
|
+
|
85
|
+
Step 1: Frame the problem as a tension between two opposing approaches, where:
|
86
|
+
- Thesis (T): The first approach or position
|
87
|
+
- Antithesis (A): The contrasting approach or position
|
88
|
+
|
89
|
+
The solution that is suggested or implied in the text must represent the Linear Action (Ac) that transforms the negative aspect of the thesis (T-) into the positive aspect of the antithesis (A+)
|
90
|
+
|
91
|
+
Step 2: Create a Dialectical Reflection (Re):
|
92
|
+
- A complementary solution that is NOT present in the analyzed text
|
93
|
+
- This solution should transform the negative aspect of the antithesis (A-) into the positive aspect of the thesis (T+)
|
94
|
+
- It should work harmoniously with the Linear Action to create a more complete solution
|
95
|
+
|
96
|
+
<example>
|
97
|
+
For example:
|
98
|
+
In a token vesting dispute, stakeholders disagreed about extending the lock period from January 2025 to January 2026. The original solution was a staged distribution with incentives.
|
99
|
+
|
100
|
+
Thesis T: Vest Now
|
101
|
+
T+ = Trust Building
|
102
|
+
T- = Loss of Value
|
103
|
+
|
104
|
+
Antithesis A: Vest Later
|
105
|
+
A+ = Value Protection (contradicts T-)
|
106
|
+
A- = Trust Erosion (contradicts T+)
|
107
|
+
|
108
|
+
Linear Action: Staged distribution with added incentives, offering 25% immediate unlock with enhanced benefits for the delayed 75% portion.
|
109
|
+
|
110
|
+
Dialectical Reflection: Liquid staking derivatives for immediate utility (25%) combined with guaranteed exit rights (75%) - complements the linear action.
|
111
|
+
</example>
|
112
|
+
</instructions>
|
113
|
+
|
114
|
+
<formatting>
|
115
|
+
Output Linear Action and Dialectical Reflection as a fluent text that could be useful for someone who provided the initial context. Compose the problem statement in the passive voice. Don't mention any special denotations such as "T", "T+", "A-", "Ac", "Re", etc.
|
116
|
+
</formatting>
|
117
|
+
"""
|
118
|
+
)
|
119
|
+
def reciprocal_solution_prompt(self, text: str, focus: WisdomUnit) -> Messages.Type:
|
120
|
+
# TODO: do we want to include the whole wheel reengineered? Also transitions so far?
|
121
|
+
return {
|
122
|
+
"computed_fields": {
|
123
|
+
"text": text,
|
124
|
+
"dialectical_analysis": focus.pretty(),
|
125
|
+
"component_length": self.settings.component_length,
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
@with_langfuse()
|
130
|
+
@use_brain(
|
131
|
+
response_model=ReciprocalSolutionDto,
|
132
|
+
)
|
133
|
+
async def reciprocal_solution(self, focus: WisdomUnit):
|
134
|
+
return self.reciprocal_solution_prompt(self._text, focus=focus)
|
135
|
+
|
136
|
+
@with_langfuse()
|
137
|
+
@use_brain(response_model=DialecticalComponentsDeckDto)
|
138
|
+
async def action_reflection(self, focus: WisdomUnit):
|
139
|
+
return self.ac_re_prompt(self._text, focus=focus)
|
140
|
+
|
141
|
+
async def think(self, focus: WheelSegment) -> List[TransitionSegmentToSegment]:
|
142
|
+
wu = self._wheel.wisdom_unit_at(focus)
|
143
|
+
|
144
|
+
async_reasoning_threads = [
|
145
|
+
self.action_reflection(focus=wu),
|
146
|
+
self.reciprocal_solution(focus=wu)
|
147
|
+
]
|
148
|
+
|
149
|
+
dc_deck_dto: DialecticalComponentsDeckDto
|
150
|
+
reciprocal_sol_dto: ReciprocalSolutionDto
|
151
|
+
dc_deck_dto, reciprocal_sol_dto = await asyncio.gather(*async_reasoning_threads)
|
152
|
+
|
153
|
+
ac_re_wu = WisdomUnit(
|
154
|
+
reasoning_mode=DialecticalReasoningMode.ACTION_REFLECTION,
|
155
|
+
rationales=[Rationale(text=reciprocal_sol_dto.problem)]
|
156
|
+
)
|
157
|
+
dialectical_components: list[DialecticalComponent] = map_list_from_dto(dc_deck_dto.dialectical_components, DialecticalComponent)
|
158
|
+
for dc in dialectical_components:
|
159
|
+
alias = self._translate_to_canonical_alias(dc.alias)
|
160
|
+
setattr(ac_re_wu, alias, dc)
|
161
|
+
|
162
|
+
graph = DirectedGraph[TransitionSegmentToSegment]()
|
163
|
+
graph.add_transition(
|
164
|
+
TransitionSegmentToSegment(
|
165
|
+
predicate=Predicate.TRANSFORMS_TO,
|
166
|
+
source_aliases=[wu.t_minus.alias, wu.t.alias],
|
167
|
+
target_aliases=[wu.a_plus.alias],
|
168
|
+
source=wu.extract_segment_t(),
|
169
|
+
target=wu.extract_segment_a(),
|
170
|
+
rationales=[
|
171
|
+
Rationale(text=reciprocal_sol_dto.linear_action)
|
172
|
+
],
|
173
|
+
)
|
174
|
+
)
|
175
|
+
graph.add_transition(
|
176
|
+
TransitionSegmentToSegment(
|
177
|
+
predicate=Predicate.TRANSFORMS_TO,
|
178
|
+
source_aliases=[wu.a_minus.alias, wu.a.alias],
|
179
|
+
target_aliases=[wu.t_plus.alias],
|
180
|
+
source=wu.extract_segment_a(),
|
181
|
+
target=wu.extract_segment_t(),
|
182
|
+
rationales=[
|
183
|
+
Rationale(text=reciprocal_sol_dto.dialectical_reflection)
|
184
|
+
],
|
185
|
+
)
|
186
|
+
)
|
187
|
+
|
188
|
+
# TODO: maybe we should rather merge if there was a transformation already (e.g. as a separate rationale?)
|
189
|
+
wu.transformation = Transformation(
|
190
|
+
ac_re=ac_re_wu,
|
191
|
+
graph=graph
|
192
|
+
)
|
193
|
+
|
194
|
+
# We return empty, because we're not merging anything, and we're sure there will be nothing to do with the result
|
195
|
+
return []
|
196
|
+
|
197
|
+
@staticmethod
|
198
|
+
def _translate_to_canonical_alias(alias: str) -> str:
|
199
|
+
if alias == ALIAS_AC:
|
200
|
+
return ALIAS_T
|
201
|
+
|
202
|
+
if alias == ALIAS_AC_PLUS:
|
203
|
+
return ALIAS_T_PLUS
|
204
|
+
|
205
|
+
if alias == ALIAS_AC_MINUS:
|
206
|
+
return ALIAS_T_MINUS
|
207
|
+
|
208
|
+
if alias == ALIAS_RE:
|
209
|
+
return ALIAS_A
|
210
|
+
|
211
|
+
if alias == ALIAS_RE_PLUS:
|
212
|
+
return ALIAS_A_PLUS
|
213
|
+
|
214
|
+
if alias == ALIAS_RE_MINUS:
|
215
|
+
return ALIAS_A_MINUS
|
216
|
+
|
217
|
+
return alias
|