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.
Files changed (84) hide show
  1. dialectical_framework/__init__.py +43 -0
  2. dialectical_framework/ai_dto/__init__.py +0 -0
  3. dialectical_framework/ai_dto/causal_cycle_assessment_dto.py +16 -0
  4. dialectical_framework/ai_dto/causal_cycle_dto.py +16 -0
  5. dialectical_framework/ai_dto/causal_cycles_deck_dto.py +11 -0
  6. dialectical_framework/ai_dto/dialectical_component_dto.py +16 -0
  7. dialectical_framework/ai_dto/dialectical_components_deck_dto.py +12 -0
  8. dialectical_framework/ai_dto/dto_mapper.py +103 -0
  9. dialectical_framework/ai_dto/reciprocal_solution_dto.py +24 -0
  10. dialectical_framework/analyst/__init__.py +1 -0
  11. dialectical_framework/analyst/audit/__init__.py +0 -0
  12. dialectical_framework/analyst/consultant.py +30 -0
  13. dialectical_framework/analyst/decorator_action_reflection.py +28 -0
  14. dialectical_framework/analyst/decorator_discrete_spiral.py +30 -0
  15. dialectical_framework/analyst/domain/__init__.py +0 -0
  16. dialectical_framework/analyst/domain/assessable_cycle.py +128 -0
  17. dialectical_framework/analyst/domain/cycle.py +133 -0
  18. dialectical_framework/analyst/domain/interpretation.py +16 -0
  19. dialectical_framework/analyst/domain/rationale.py +116 -0
  20. dialectical_framework/analyst/domain/spiral.py +47 -0
  21. dialectical_framework/analyst/domain/transformation.py +24 -0
  22. dialectical_framework/analyst/domain/transition.py +160 -0
  23. dialectical_framework/analyst/domain/transition_cell_to_cell.py +29 -0
  24. dialectical_framework/analyst/domain/transition_segment_to_segment.py +16 -0
  25. dialectical_framework/analyst/strategic_consultant.py +32 -0
  26. dialectical_framework/analyst/think_action_reflection.py +217 -0
  27. dialectical_framework/analyst/think_constructive_convergence.py +87 -0
  28. dialectical_framework/analyst/wheel_builder_transition_calculator.py +157 -0
  29. dialectical_framework/brain.py +81 -0
  30. dialectical_framework/dialectical_analysis.py +14 -0
  31. dialectical_framework/dialectical_component.py +111 -0
  32. dialectical_framework/dialectical_components_deck.py +48 -0
  33. dialectical_framework/dialectical_reasoning.py +105 -0
  34. dialectical_framework/directed_graph.py +419 -0
  35. dialectical_framework/enums/__init__.py +0 -0
  36. dialectical_framework/enums/causality_type.py +10 -0
  37. dialectical_framework/enums/di.py +11 -0
  38. dialectical_framework/enums/dialectical_reasoning_mode.py +9 -0
  39. dialectical_framework/enums/predicate.py +9 -0
  40. dialectical_framework/protocols/__init__.py +0 -0
  41. dialectical_framework/protocols/assessable.py +141 -0
  42. dialectical_framework/protocols/causality_sequencer.py +111 -0
  43. dialectical_framework/protocols/content_fidelity_evaluator.py +16 -0
  44. dialectical_framework/protocols/has_brain.py +18 -0
  45. dialectical_framework/protocols/has_config.py +18 -0
  46. dialectical_framework/protocols/ratable.py +79 -0
  47. dialectical_framework/protocols/reloadable.py +7 -0
  48. dialectical_framework/protocols/thesis_extractor.py +15 -0
  49. dialectical_framework/settings.py +77 -0
  50. dialectical_framework/synthesis.py +7 -0
  51. dialectical_framework/synthesist/__init__.py +1 -0
  52. dialectical_framework/synthesist/causality/__init__.py +0 -0
  53. dialectical_framework/synthesist/causality/causality_sequencer_balanced.py +398 -0
  54. dialectical_framework/synthesist/causality/causality_sequencer_desirable.py +55 -0
  55. dialectical_framework/synthesist/causality/causality_sequencer_feasible.py +55 -0
  56. dialectical_framework/synthesist/causality/causality_sequencer_realistic.py +56 -0
  57. dialectical_framework/synthesist/concepts/__init__.py +0 -0
  58. dialectical_framework/synthesist/concepts/thesis_extractor_basic.py +135 -0
  59. dialectical_framework/synthesist/polarity/__init__.py +0 -0
  60. dialectical_framework/synthesist/polarity/polarity_reasoner.py +700 -0
  61. dialectical_framework/synthesist/polarity/reason_blind.py +62 -0
  62. dialectical_framework/synthesist/polarity/reason_conversational.py +91 -0
  63. dialectical_framework/synthesist/polarity/reason_fast.py +177 -0
  64. dialectical_framework/synthesist/polarity/reason_fast_and_simple.py +52 -0
  65. dialectical_framework/synthesist/polarity/reason_fast_polarized_conflict.py +55 -0
  66. dialectical_framework/synthesist/reverse_engineer.py +401 -0
  67. dialectical_framework/synthesist/wheel_builder.py +337 -0
  68. dialectical_framework/utils/__init__.py +1 -0
  69. dialectical_framework/utils/dc_replace.py +42 -0
  70. dialectical_framework/utils/decompose_probability_uniformly.py +15 -0
  71. dialectical_framework/utils/extend_tpl.py +12 -0
  72. dialectical_framework/utils/gm.py +12 -0
  73. dialectical_framework/utils/is_async.py +13 -0
  74. dialectical_framework/utils/use_brain.py +80 -0
  75. dialectical_framework/validator/__init__.py +1 -0
  76. dialectical_framework/validator/basic_checks.py +75 -0
  77. dialectical_framework/validator/check.py +12 -0
  78. dialectical_framework/wheel.py +395 -0
  79. dialectical_framework/wheel_segment.py +203 -0
  80. dialectical_framework/wisdom_unit.py +185 -0
  81. dialectical_framework-0.4.4.dist-info/LICENSE +21 -0
  82. dialectical_framework-0.4.4.dist-info/METADATA +123 -0
  83. dialectical_framework-0.4.4.dist-info/RECORD +84 -0
  84. 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