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,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]
|