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,43 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Import all core Pydantic models that participate in potential circular dependencies.
4
+ # The order of imports here ensures classes are defined before their `.model_rebuild()`
5
+ # methods are called below.
6
+ from .analyst.domain.assessable_cycle import AssessableCycle
7
+ from .analyst.domain.cycle import Cycle
8
+ from .analyst.domain.rationale import Rationale
9
+ from .analyst.domain.spiral import Spiral
10
+ from .analyst.domain.transformation import Transformation
11
+ from .analyst.domain.transition import Transition
12
+ from .analyst.domain.transition_cell_to_cell import TransitionCellToCell
13
+ from .analyst.domain.transition_segment_to_segment import \
14
+ TransitionSegmentToSegment
15
+ from .dialectical_component import DialecticalComponent
16
+ from .protocols.assessable import Assessable
17
+ from .protocols.ratable import Ratable
18
+ from .synthesis import Synthesis
19
+ from .wheel import Wheel
20
+ from .wheel_segment import WheelSegment
21
+ from .wisdom_unit import WisdomUnit
22
+
23
+ # Explicitly call `model_rebuild()` on all models that might have forward references
24
+ # or be part of circular dependencies. This forces Pydantic to resolve their schemas
25
+ # after all classes are defined in the module.
26
+ # The order of these rebuild calls is generally from base classes to derived classes,
27
+ # or simply ensuring all interdependent models are covered.
28
+
29
+ Assessable.model_rebuild()
30
+ Ratable.model_rebuild()
31
+ DialecticalComponent.model_rebuild()
32
+ Rationale.model_rebuild()
33
+ Synthesis.model_rebuild()
34
+ Wheel.model_rebuild()
35
+ WheelSegment.model_rebuild()
36
+ Transition.model_rebuild()
37
+ TransitionCellToCell.model_rebuild()
38
+ TransitionSegmentToSegment.model_rebuild()
39
+ AssessableCycle.model_rebuild()
40
+ Cycle.model_rebuild()
41
+ Spiral.model_rebuild()
42
+ Transformation.model_rebuild()
43
+ WisdomUnit.model_rebuild()
File without changes
@@ -0,0 +1,16 @@
1
+
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class CausalCycleAssessmentDto(BaseModel):
6
+ probability: float = Field(
7
+ default=0,
8
+ description="The probability 0 to 1 of the arranged cycle to exist in reality.",
9
+ )
10
+ reasoning_explanation: str = Field(
11
+ default="", description="Explanation why/how this cycle might occur."
12
+ )
13
+ argumentation: str = Field(
14
+ default="",
15
+ description="Circumstances or contexts where this cycle would be most applicable or useful.",
16
+ )
@@ -0,0 +1,16 @@
1
+
2
+ from pydantic import Field
3
+
4
+ from dialectical_framework.ai_dto.causal_cycle_assessment_dto import \
5
+ CausalCycleAssessmentDto
6
+
7
+
8
+ class CausalCycleDto(CausalCycleAssessmentDto):
9
+ """
10
+ Causal circular sequence of statements, where aliases reference each statement
11
+ """
12
+
13
+ aliases: list[str] = Field(
14
+ ...,
15
+ description="Aliases (not the explicit statements) arranged in the circular causality sequence (cycle) where the last element points to the first",
16
+ )
@@ -0,0 +1,11 @@
1
+
2
+ from pydantic import BaseModel, Field
3
+
4
+ from dialectical_framework.ai_dto.causal_cycle_dto import CausalCycleDto
5
+
6
+
7
+ class CausalCyclesDeckDto(BaseModel):
8
+ causal_cycles: list[CausalCycleDto] = Field(
9
+ ...,
10
+ description="A list of causal circular sequences (cycles). It might also be filled with only one if only one is to be found.",
11
+ )
@@ -0,0 +1,16 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class DialecticalComponentDto(BaseModel):
5
+ alias: str = Field(
6
+ ...,
7
+ description="The user friendly name of the dialectical component such as T, A, T+, A+, etc.",
8
+ )
9
+ statement: str = Field(
10
+ ...,
11
+ description="The dialectical component value that is provided after analysis.",
12
+ )
13
+ explanation: str = Field(
14
+ default="",
15
+ description="The explanation how the dialectical component (statement) is derived.",
16
+ )
@@ -0,0 +1,12 @@
1
+
2
+ from pydantic import BaseModel, Field
3
+
4
+ from dialectical_framework.ai_dto.dialectical_component_dto import \
5
+ DialecticalComponentDto
6
+
7
+
8
+ class DialecticalComponentsDeckDto(BaseModel):
9
+ dialectical_components: list[DialecticalComponentDto] = Field(
10
+ ...,
11
+ description="A list of dialectical components. It can be empty when no dialectical components are found. It might also be filled with only one dialectical component if only one is to be found.",
12
+ )
@@ -0,0 +1,103 @@
1
+ from typing import Dict, Generic, Type, TypeVar
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from dialectical_framework import DialecticalComponent, Rationale
6
+ from dialectical_framework.ai_dto.dialectical_component_dto import DialecticalComponentDto
7
+
8
+ # Type variables for generic mapping
9
+ DtoType = TypeVar('DtoType', bound=BaseModel)
10
+ ModelType = TypeVar('ModelType', bound=BaseModel)
11
+
12
+
13
+ class DtoMapper(Generic[DtoType, ModelType]):
14
+ """
15
+ Generic auto-mapper that uses field introspection to map between DTOs and models.
16
+ Works when models have compatible field names.
17
+ """
18
+
19
+ def __init__(self, dto_class: Type[DtoType], model_class: Type[ModelType]):
20
+ self.dto_class = dto_class
21
+ self.model_class = model_class
22
+
23
+ def map_from_dto(self, dto: DtoType, **kwargs) -> ModelType:
24
+ """
25
+ Auto-maps from DTO to model using common fields plus any additional kwargs.
26
+ """
27
+ # Get DTO field values
28
+ dto_data = dto.model_dump()
29
+
30
+ # Merge with additional kwargs (kwargs take precedence)
31
+ merged_data = {**dto_data, **kwargs}
32
+
33
+ # Filter to only include fields that exist in the target model
34
+ model_fields = self.model_class.model_fields.keys()
35
+ filtered_data = {k: v for k, v in merged_data.items() if k in model_fields}
36
+
37
+ return self.model_class(**filtered_data)
38
+
39
+ def map_list_from_dto(self, dtos: list[DtoType], **kwargs) -> list[ModelType]:
40
+ """
41
+ Maps a list of DTOs to domain models.
42
+
43
+ Args:
44
+ dtos: List of DTO instances to convert
45
+ **kwargs: Additional parameters for mapping
46
+
47
+ Returns:
48
+ List of domain model instances
49
+ """
50
+ return [self.map_from_dto(dto, **kwargs) for dto in dtos]
51
+
52
+ class DialecticalComponentMapper(DtoMapper[DialecticalComponentDto, DialecticalComponent]):
53
+ def map_from_dto(self, dto: DialecticalComponentDto, **kwargs) -> ModelType:
54
+ mapped: DialecticalComponent = super().map_from_dto(dto, **kwargs)
55
+ if dto.explanation:
56
+ mapped.rationales.append(Rationale(
57
+ text=dto.explanation,
58
+ ))
59
+ return mapped
60
+
61
+ # Registry for mappers
62
+ _mapper_registry: Dict[tuple[Type, Type], DtoMapper] = {
63
+ # Use default DtoMapper if no specific mapper is registered
64
+
65
+ (DialecticalComponentDto, DialecticalComponent): DialecticalComponentMapper(DialecticalComponentDto, DialecticalComponent),
66
+ }
67
+
68
+
69
+ def register_mapper(dto_class: Type[DtoType], model_class: Type[ModelType], mapper: DtoMapper[DtoType, ModelType]):
70
+ """
71
+ Register a mapper for a specific DTO-Model pair.
72
+ """
73
+ _mapper_registry[(dto_class, model_class)] = mapper
74
+
75
+
76
+ def get_mapper(dto_class: Type[DtoType], model_class: Type[ModelType]) -> DtoMapper[DtoType, ModelType]:
77
+ """
78
+ Get a mapper for a specific DTO-Model pair.
79
+ If no specific mapper is registered, returns an AutoMapper.
80
+ """
81
+ key = (dto_class, model_class)
82
+ if key in _mapper_registry:
83
+ return _mapper_registry[key]
84
+
85
+ # Return auto-mapper as fallback
86
+ return DtoMapper(dto_class, model_class)
87
+
88
+
89
+ def map_from_dto(dto: DtoType, model_class: Type[ModelType], **kwargs) -> ModelType:
90
+ """
91
+ Convenience function to map from DTO to model.
92
+ """
93
+ mapper = get_mapper(type(dto), model_class)
94
+ return mapper.map_from_dto(dto, **kwargs)
95
+
96
+ def map_list_from_dto(dtos: list[DtoType], model_class: Type[ModelType], **kwargs) -> list[ModelType]:
97
+ """
98
+ Convenience function to map a list of DTOs to domain models.
99
+ """
100
+ if not dtos:
101
+ return []
102
+ mapper = get_mapper(type(dtos[0]), model_class)
103
+ return mapper.map_list_from_dto(dtos, **kwargs)
@@ -0,0 +1,24 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+
4
+ class ReciprocalSolutionDto(BaseModel):
5
+ model_config = ConfigDict(
6
+ extra="forbid",
7
+ )
8
+ problem: str | None = Field(default=None, description="Problem statement")
9
+ linear_action: str | None = Field(
10
+ default=None, description="Solution(s) that transforms T- into A+"
11
+ )
12
+ dialectical_reflection: str | None = Field(
13
+ default=None, description="Complementary solution(s) that transforms A- into T+"
14
+ )
15
+
16
+ def __str__(self):
17
+ str_pieces = []
18
+ if self.problem:
19
+ str_pieces.append(f"Problem: {self.problem}")
20
+ if self.linear_action:
21
+ str_pieces.append(f"Linear action: {self.linear_action}")
22
+ if self.dialectical_reflection:
23
+ str_pieces.append(f"Dialectical reflection: {self.dialectical_reflection}")
24
+ return "\n".join(str_pieces)
@@ -0,0 +1 @@
1
+
File without changes
@@ -0,0 +1,30 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
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
+
9
+
10
+ class Consultant(ABC, HasBrain):
11
+ def __init__(
12
+ self,
13
+ *,
14
+ text: str,
15
+ wheel: Wheel,
16
+ brain: Optional[Brain] = None,
17
+ ):
18
+ self._text = text
19
+ self._wheel = wheel
20
+ self._brain = brain
21
+
22
+ @property
23
+ def brain(self) -> Brain:
24
+ return super().brain if self._brain is None else self._brain
25
+
26
+ @abstractmethod
27
+ async def rationalize(self, transition: Transition) -> Transition: ...
28
+ """
29
+ The main method of the class. It should return an enriched Transition with the new rationale.
30
+ """
@@ -0,0 +1,28 @@
1
+ from dialectical_framework.analyst.domain.transition_segment_to_segment import \
2
+ TransitionSegmentToSegment
3
+ from dialectical_framework.analyst.think_action_reflection import \
4
+ ThinkActionReflection
5
+ from dialectical_framework.analyst.wheel_builder_transition_calculator import \
6
+ WheelBuilderTransitionCalculator
7
+ from dialectical_framework.wheel import Wheel
8
+ from dialectical_framework.wheel_segment import WheelSegment
9
+
10
+
11
+ class DecoratorActionReflection(WheelBuilderTransitionCalculator):
12
+ async def _do_calculate_transitions(
13
+ self, wheel: Wheel, at: WheelSegment
14
+ ) -> list[TransitionSegmentToSegment]:
15
+ consultant = ThinkActionReflection(
16
+ text=self.text, wheel=wheel, brain=self.reasoner.brain
17
+ )
18
+
19
+ return await consultant.think(focus=at)
20
+
21
+ async def _do_calculate_transitions_all(
22
+ self, wheel: Wheel
23
+ ) -> list[TransitionSegmentToSegment]:
24
+ result: list[TransitionSegmentToSegment] = []
25
+ for wu in wheel.wisdom_units:
26
+ tr = await self._do_calculate_transitions(wheel, wu)
27
+ result.extend(tr)
28
+ return result
@@ -0,0 +1,30 @@
1
+
2
+ from dialectical_framework.analyst.domain.transition_segment_to_segment import \
3
+ TransitionSegmentToSegment
4
+ from dialectical_framework.analyst.think_constructive_convergence import \
5
+ ThinkConstructiveConvergence
6
+ from dialectical_framework.analyst.wheel_builder_transition_calculator import \
7
+ WheelBuilderTransitionCalculator
8
+ from dialectical_framework.wheel import Wheel
9
+ from dialectical_framework.wheel_segment import WheelSegment
10
+
11
+
12
+ class DecoratorDiscreteSpiral(WheelBuilderTransitionCalculator):
13
+ async def _do_calculate_transitions(
14
+ self, wheel: Wheel, at: WheelSegment
15
+ ) -> list[TransitionSegmentToSegment]:
16
+ consultant = ThinkConstructiveConvergence(
17
+ text=self.text, wheel=wheel, brain=self.reasoner.brain
18
+ )
19
+
20
+ return [await consultant.think(at)]
21
+
22
+ async def _do_calculate_transitions_all(
23
+ self, wheel: Wheel
24
+ ) -> list[TransitionSegmentToSegment]:
25
+ # TODO: use a single prompt to derive all transitions faster?
26
+ result: list[TransitionSegmentToSegment] = []
27
+ for i in range(wheel.degree):
28
+ tr = await self._do_calculate_transitions(wheel, wheel.wheel_segment_at(i))
29
+ result.extend(tr)
30
+ return result
File without changes
@@ -0,0 +1,128 @@
1
+ from abc import ABC
2
+
3
+ from pydantic import ConfigDict, Field
4
+
5
+ from dialectical_framework.analyst.domain.transition import Transition
6
+ from dialectical_framework.directed_graph import DirectedGraph
7
+ from dialectical_framework.protocols.assessable import Assessable
8
+ from dialectical_framework.utils.decompose_probability_uniformly import decompose_probability_uniformly
9
+
10
+
11
+ class AssessableCycle(Assessable, ABC):
12
+ model_config = ConfigDict(
13
+ arbitrary_types_allowed=True,
14
+ )
15
+
16
+ graph: DirectedGraph[Transition] = Field(
17
+ default=None,
18
+ description="Directed graph representing the cycle of dialectical components.",
19
+ )
20
+
21
+ def _get_sub_assessables(self) -> list[Assessable]:
22
+ result = super()._get_sub_assessables()
23
+ result.extend(self.graph.get_all_transitions())
24
+ return result
25
+
26
+ def _calculate_contextual_fidelity_for_sub_elements_excl_rationales(self, *, mutate: bool = True) -> list[float]:
27
+ """
28
+ Calculates the cycle fidelity (CF_S) as the geometric mean of:
29
+ 1. All dialectical components' contextual fidelity scores within the cycle's transitions
30
+ 2. All cycle-level rationales/opinions (weighted by their rating)
31
+
32
+ Components/rationales with contextual_fidelity of 0.0 or None are excluded from the calculation.
33
+ """
34
+ parts = []
35
+
36
+ # Collect fidelities from dialectical components
37
+ transitions = self.graph.get_all_transitions()
38
+ if transitions:
39
+ for transition in transitions:
40
+ parts.append(transition.calculate_contextual_fidelity(mutate=mutate))
41
+
42
+ return parts
43
+
44
+ def calculate_probability(self, *, mutate: bool = True) -> float | None:
45
+ """
46
+ Pr(Cycle) = product of ALL transition probabilities, in order.
47
+ - If any transition Pr is 0.0 -> 0.0 (hard veto)
48
+ - If any transition Pr is None -> None (unknown)
49
+ - Else product of all
50
+ No cycle-level opinions here.
51
+ """
52
+ transitions: list[Transition] = self.graph.first_path() # ensure this is the ordered full cycle
53
+ if not transitions:
54
+ prob = None
55
+ else:
56
+ prob = 1.0
57
+ for tr in transitions:
58
+ p = tr.calculate_probability(mutate=mutate)
59
+ if p is None:
60
+ prob = None
61
+ break
62
+ if p == 0.0:
63
+ prob = 0.0
64
+ break
65
+ prob *= p
66
+
67
+ if mutate:
68
+ self.probability = prob
69
+ return prob
70
+
71
+ def decompose_probability_into_transitions(
72
+ probability: float,
73
+ transitions: list[Transition],
74
+ overwrite_existing_transition_probabilities: bool = False
75
+ ) -> None:
76
+ """
77
+ **Case 1: No existing probabilities**
78
+ - Uses uniform decomposition: `cycle_prob^(1/n)` for all transitions
79
+
80
+ **Case 2: All transitions have probabilities**
81
+ - Does nothing - respects existing assignments
82
+
83
+ **Case 3: Mixed (some have, some don't)**
84
+ - Calculates "remaining probability" after accounting for assigned ones
85
+ - Distributes remaining probability uniformly among unassigned transitions
86
+
87
+ """
88
+ if not transitions:
89
+ return
90
+
91
+ if overwrite_existing_transition_probabilities:
92
+ for t in transitions:
93
+ t.probability = None
94
+
95
+ # Check which transitions already have probabilities
96
+ transitions_with_probs = [t for t in transitions if t.probability is not None and t.probability != 0]
97
+ transitions_without_probs = [t for t in transitions if t.probability is None or t.probability == 0]
98
+
99
+ if not transitions_without_probs:
100
+ # All transitions already have probabilities - don't override
101
+ return
102
+
103
+ if not transitions_with_probs:
104
+ # No transitions have probabilities - use uniform decomposition
105
+ individual_prob = decompose_probability_uniformly(
106
+ probability,
107
+ len(transitions)
108
+ )
109
+ for transition in transitions:
110
+ transition.manual_probability = individual_prob
111
+ else:
112
+ # Mixed case: some have probabilities, some don't
113
+ # Calculate what's "left over" for the unassigned transitions
114
+ assigned_prob_product = 1.0
115
+ for transition in transitions_with_probs:
116
+ assigned_prob_product *= transition.probability
117
+
118
+ # Remaining probability to distribute
119
+ remaining_prob = probability / assigned_prob_product if assigned_prob_product > 0 else probability
120
+
121
+ # Distribute remaining probability uniformly among unassigned transitions
122
+ if transitions_without_probs and remaining_prob > 0:
123
+ individual_prob = decompose_probability_uniformly(
124
+ remaining_prob,
125
+ len(transitions_without_probs)
126
+ )
127
+ for transition in transitions_without_probs:
128
+ transition.probability = individual_prob
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from dialectical_framework.analyst.domain.assessable_cycle import \
8
+ AssessableCycle
9
+ from dialectical_framework.analyst.domain.transition_cell_to_cell import \
10
+ TransitionCellToCell
11
+ from dialectical_framework.dialectical_component import DialecticalComponent
12
+ from dialectical_framework.dialectical_components_deck import \
13
+ DialecticalComponentsDeck
14
+ from dialectical_framework.directed_graph import DirectedGraph
15
+ from dialectical_framework.enums.causality_type import CausalityType
16
+ from dialectical_framework.enums.predicate import Predicate
17
+
18
+
19
+ class Cycle(AssessableCycle):
20
+ model_config = ConfigDict(
21
+ extra="forbid",
22
+ arbitrary_types_allowed=True,
23
+ )
24
+
25
+ causality_type: CausalityType = Field(
26
+ ..., description="The type of causality in the cycle."
27
+ )
28
+ causality_direction: Literal["clockwise", "counterclockwise"] = Field(
29
+ default="clockwise", description="The direction of causality in the ring."
30
+ )
31
+
32
+ argumentation: str = Field(
33
+ default="",
34
+ description="Circumstances or contexts where this cycle would be most applicable or useful.",
35
+ )
36
+
37
+ def __init__(
38
+ self,
39
+ dialectical_components: list[DialecticalComponent],
40
+ causality_type: CausalityType = CausalityType.REALISTIC,
41
+ **data,
42
+ ):
43
+ data["causality_type"] = causality_type
44
+ super().__init__(**data)
45
+ if self.graph is None:
46
+ self.graph = DirectedGraph[TransitionCellToCell]()
47
+ for i in range(len(dialectical_components)):
48
+ next_i = (i + 1) % len(dialectical_components)
49
+ if self.causality_direction == "clockwise":
50
+ source = dialectical_components[i]
51
+ target = dialectical_components[next_i]
52
+ else:
53
+ source = dialectical_components[next_i]
54
+ target = dialectical_components[i]
55
+
56
+ self.graph.add_transition(
57
+ TransitionCellToCell(
58
+ source=source,
59
+ predicate=Predicate.CAUSES,
60
+ target=target,
61
+ # TODO: how do we set the transition text?
62
+ )
63
+ )
64
+
65
+ @property
66
+ def dialectical_components(self) -> list[DialecticalComponent]:
67
+ """Returns list of dialectical components from the first path of the ring."""
68
+ path = self.graph.first_path()
69
+ return [transition.source for transition in path] if path else []
70
+
71
+ def cycle_str(self) -> str:
72
+ """Returns a string representation of the cycle sequence."""
73
+ aliases = [dc.alias for dc in self.dialectical_components]
74
+ if not aliases:
75
+ return ""
76
+ if len(aliases) == 1:
77
+ return f"{aliases[0]} → {aliases[0]}..."
78
+ return " → ".join(aliases) + f" → {aliases[0]}..."
79
+
80
+ def is_same_structure(self, other: Cycle) -> bool:
81
+ """Check if cycles represent the same sequence regardless of starting point."""
82
+ self_aliases = DialecticalComponentsDeck(
83
+ dialectical_components=self.dialectical_components
84
+ ).get_aliases()
85
+
86
+ other_aliases = DialecticalComponentsDeck(
87
+ dialectical_components=other.dialectical_components
88
+ ).get_aliases()
89
+
90
+ # Same length check
91
+ if len(self_aliases) != len(other_aliases):
92
+ return False
93
+
94
+ # Convert to sets for same elements check
95
+ if set(self_aliases) != set(other_aliases):
96
+ return False
97
+
98
+ # Check rotations only if sets are equal
99
+ if len(self_aliases) <= 1:
100
+ return True
101
+
102
+ return any(
103
+ self_aliases == other_aliases[i:] + other_aliases[:i]
104
+ for i in range(len(other_aliases))
105
+ )
106
+
107
+ def pretty(
108
+ self,
109
+ *,
110
+ skip_dialectical_component_explanation=False,
111
+ start_alias: str | DialecticalComponent | None = None,
112
+ ) -> str:
113
+ output = [self.graph.pretty() + f" | Probability: {self.probability}"]
114
+
115
+ path = self.graph.first_path(
116
+ start_aliases=[start_alias] if start_alias else None
117
+ )
118
+ if not path:
119
+ raise ValueError(
120
+ f"No path found between {start_alias} and the first dialectical component in the cycle."
121
+ )
122
+ for transition in path:
123
+ dc = transition.source
124
+ output.append(
125
+ dc.pretty(skip_explanation=skip_dialectical_component_explanation)
126
+ )
127
+
128
+ output.append(f"Rationale: {self.best_rationale.text if self.best_rationale else ''}")
129
+
130
+ return "\n".join(output)
131
+
132
+ def __str__(self):
133
+ return self.pretty()
@@ -0,0 +1,16 @@
1
+
2
+ from pydantic import BaseModel, Field
3
+
4
+ from dialectical_framework.analyst.domain.cycle import Cycle
5
+ from dialectical_framework.analyst.domain.spiral import Spiral
6
+ from dialectical_framework.analyst.domain.transformation import Transformation
7
+ from dialectical_framework.wheel import Wheel
8
+
9
+
10
+ class Interpretation(BaseModel):
11
+ wheel: Wheel
12
+ coherence: float = Field(default=0.0, description="geometric mean of all cycle probabilities")
13
+ t_cycle: Cycle
14
+ ta_cycle: Cycle
15
+ spiral: Spiral
16
+ transformations: list[Transformation]