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,337 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Dict, Union
|
4
|
+
|
5
|
+
from dependency_injector.wiring import Provide
|
6
|
+
|
7
|
+
from dialectical_framework.ai_dto.dto_mapper import (map_from_dto,
|
8
|
+
map_list_from_dto)
|
9
|
+
from dialectical_framework.analyst.domain.cycle import Cycle
|
10
|
+
from dialectical_framework.dialectical_component import DialecticalComponent
|
11
|
+
from dialectical_framework.dialectical_components_deck import \
|
12
|
+
DialecticalComponentsDeck
|
13
|
+
from dialectical_framework.enums.di import DI
|
14
|
+
from dialectical_framework.protocols.causality_sequencer import \
|
15
|
+
CausalitySequencer
|
16
|
+
from dialectical_framework.protocols.has_config import SettingsAware
|
17
|
+
from dialectical_framework.protocols.thesis_extractor import ThesisExtractor
|
18
|
+
from dialectical_framework.synthesis import (ALIAS_S_MINUS, ALIAS_S_PLUS, Synthesis)
|
19
|
+
from dialectical_framework.synthesist.polarity.polarity_reasoner import \
|
20
|
+
PolarityReasoner
|
21
|
+
from dialectical_framework.wheel import Wheel, WheelSegmentReference
|
22
|
+
from dialectical_framework.wheel_segment import ALIAS_T
|
23
|
+
from dialectical_framework.wisdom_unit import WisdomUnit
|
24
|
+
|
25
|
+
|
26
|
+
class WheelBuilder(SettingsAware):
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
thesis_extractor: ThesisExtractor = Provide[DI.thesis_extractor],
|
30
|
+
causality_sequencer: CausalitySequencer = Provide[DI.causality_sequencer],
|
31
|
+
polarity_reasoner: PolarityReasoner = Provide[DI.polarity_reasoner],
|
32
|
+
*,
|
33
|
+
text: str = "",
|
34
|
+
wheels: list[Wheel] = None,
|
35
|
+
):
|
36
|
+
self.__text = text
|
37
|
+
self.__wheels: list[Wheel] = wheels or []
|
38
|
+
|
39
|
+
# TODO: reloading singletons isn't very good design here, because we're guessing the parameters...
|
40
|
+
|
41
|
+
self.__extractor = thesis_extractor
|
42
|
+
self.__extractor.reload(text=text)
|
43
|
+
|
44
|
+
self.__sequencer = causality_sequencer
|
45
|
+
self.__sequencer.reload(text=text)
|
46
|
+
|
47
|
+
self.__reasoner = polarity_reasoner
|
48
|
+
self.__reasoner.reload(text=text)
|
49
|
+
|
50
|
+
@property
|
51
|
+
def wheel_permutations(self) -> list[Wheel]:
|
52
|
+
return self.__wheels
|
53
|
+
|
54
|
+
@property
|
55
|
+
def text(self) -> str | None:
|
56
|
+
return self.__text
|
57
|
+
|
58
|
+
@property
|
59
|
+
def extractor(self) -> ThesisExtractor:
|
60
|
+
return self.__extractor
|
61
|
+
|
62
|
+
@property
|
63
|
+
def reasoner(self) -> PolarityReasoner:
|
64
|
+
return self.__reasoner
|
65
|
+
|
66
|
+
@property
|
67
|
+
def sequencer(self) -> CausalitySequencer:
|
68
|
+
return self.__sequencer
|
69
|
+
|
70
|
+
async def t_cycles(self, *, theses: list[Union[str, None]] = None) -> list[Cycle]:
|
71
|
+
if theses is None:
|
72
|
+
# No theses provided, generate one automatically
|
73
|
+
t = await self.extractor.extract_single_thesis()
|
74
|
+
t.alias = ALIAS_T
|
75
|
+
theses = [t]
|
76
|
+
else:
|
77
|
+
# Handle mixed None and str values
|
78
|
+
final_theses: list[DialecticalComponent | None] = []
|
79
|
+
none_positions = []
|
80
|
+
|
81
|
+
# First pass: collect provided theses and identify positions that need generation
|
82
|
+
for i, thesis in enumerate(theses):
|
83
|
+
if thesis is None or (isinstance(thesis, str) and not thesis.strip()):
|
84
|
+
none_positions.append(i)
|
85
|
+
final_theses.append(None) # Placeholder
|
86
|
+
else:
|
87
|
+
provided_thesis = DialecticalComponent(
|
88
|
+
alias=f"{ALIAS_T}",
|
89
|
+
statement=thesis,
|
90
|
+
)
|
91
|
+
provided_thesis.set_human_friendly_index(i + 1)
|
92
|
+
final_theses.append(provided_thesis)
|
93
|
+
|
94
|
+
# Generate all missing theses at once if needed
|
95
|
+
if none_positions:
|
96
|
+
if len(none_positions) == 1:
|
97
|
+
# Single thesis case: place at the correct original position
|
98
|
+
pos = none_positions[0]
|
99
|
+
generated_thesis = map_from_dto(await self.reasoner.find_thesis(), DialecticalComponent)
|
100
|
+
generated_thesis.alias = ALIAS_T
|
101
|
+
generated_thesis.set_human_friendly_index(pos + 1)
|
102
|
+
final_theses[pos] = generated_thesis
|
103
|
+
else:
|
104
|
+
# Multiple theses case - extract all missing ones at once
|
105
|
+
t_deck = await self.extractor.extract_multiple_theses(
|
106
|
+
count=len(none_positions)
|
107
|
+
)
|
108
|
+
generated_theses = t_deck.dialectical_components
|
109
|
+
|
110
|
+
# Place generated theses in their correct positions
|
111
|
+
for i, pos in enumerate(none_positions):
|
112
|
+
if i < len(generated_theses):
|
113
|
+
generated_theses[i].alias = ALIAS_T
|
114
|
+
generated_theses[i].set_human_friendly_index(pos + 1)
|
115
|
+
final_theses[pos] = generated_theses[i]
|
116
|
+
|
117
|
+
# Backfill any remaining None (it's a precaution, in case fewer were generated than requested)
|
118
|
+
for pos in none_positions:
|
119
|
+
if final_theses[pos] is None:
|
120
|
+
generated_thesis = map_from_dto(await self.reasoner.find_thesis(), DialecticalComponent)
|
121
|
+
generated_thesis.alias = ALIAS_T
|
122
|
+
generated_thesis.set_human_friendly_index(pos + 1)
|
123
|
+
final_theses[pos] = generated_thesis
|
124
|
+
|
125
|
+
theses = final_theses
|
126
|
+
|
127
|
+
cycles: list[Cycle] = await self.__sequencer.arrange(theses)
|
128
|
+
return cycles
|
129
|
+
|
130
|
+
async def build_wheel_permutations(
|
131
|
+
self, *, theses: list[Union[str, None]] = None, t_cycle: Cycle = None
|
132
|
+
) -> list[Wheel]:
|
133
|
+
"""
|
134
|
+
TODO: theses are used only when t_cycle is not provided. should be single param or so. Refactor
|
135
|
+
IMPORTANT: t_cycle is the "path" we take for permutations. If not provided, we'll take the most likely path.
|
136
|
+
Do not confuse it with building all wheels for all "paths"
|
137
|
+
"""
|
138
|
+
if t_cycle is None:
|
139
|
+
cycles: list[Cycle] = await self.t_cycles(theses=theses)
|
140
|
+
# The first one is the highest probability
|
141
|
+
t_cycle = cycles[0]
|
142
|
+
|
143
|
+
wheel_wisdom_units = []
|
144
|
+
for dc in t_cycle.dialectical_components:
|
145
|
+
wu = await self.reasoner.think(thesis=dc.statement)
|
146
|
+
if dc.rationales:
|
147
|
+
wu.t.rationales.extend(dc.rationales)
|
148
|
+
|
149
|
+
idx = dc.get_human_friendly_index()
|
150
|
+
if idx:
|
151
|
+
wu.add_indexes_to_aliases(idx)
|
152
|
+
|
153
|
+
wheel_wisdom_units.append(wu)
|
154
|
+
|
155
|
+
cycles: list[Cycle] = await self.__sequencer.arrange(wheel_wisdom_units)
|
156
|
+
|
157
|
+
wheels = []
|
158
|
+
for cycle in cycles:
|
159
|
+
w = Wheel(
|
160
|
+
_rearrange_by_causal_sequence(wheel_wisdom_units, cycle, mutate=False),
|
161
|
+
t_cycle=t_cycle,
|
162
|
+
ta_cycle=cycle,
|
163
|
+
)
|
164
|
+
w.calculate_score()
|
165
|
+
wheels.append(w)
|
166
|
+
|
167
|
+
# Save results for reference
|
168
|
+
self.__wheels = wheels
|
169
|
+
return self.wheel_permutations
|
170
|
+
|
171
|
+
async def calculate_syntheses(
|
172
|
+
self,
|
173
|
+
*,
|
174
|
+
wheel: Wheel,
|
175
|
+
at: WheelSegmentReference | list[WheelSegmentReference] = None,
|
176
|
+
):
|
177
|
+
if wheel not in self.wheel_permutations:
|
178
|
+
raise ValueError(f"Wheel permutation {wheel} not found in available wheels")
|
179
|
+
|
180
|
+
wisdom_units = []
|
181
|
+
|
182
|
+
if at is None:
|
183
|
+
# Calculate for each
|
184
|
+
wisdom_units = wheel.wisdom_units
|
185
|
+
elif isinstance(at, list):
|
186
|
+
# Calculate for some
|
187
|
+
for ref in at:
|
188
|
+
wisdom_units.append(wheel.wisdom_unit_at(ref))
|
189
|
+
else:
|
190
|
+
# Calculate for one
|
191
|
+
wisdom_units.append(wheel.wisdom_unit_at(at))
|
192
|
+
|
193
|
+
for wu in wisdom_units:
|
194
|
+
ss_deck_dto = await self.reasoner.find_synthesis(wu)
|
195
|
+
ss_deck = DialecticalComponentsDeck(dialectical_components=map_list_from_dto(ss_deck_dto.dialectical_components, DialecticalComponent))
|
196
|
+
wu.synthesis = Synthesis(
|
197
|
+
t_plus=ss_deck.get_by_alias(ALIAS_S_PLUS),
|
198
|
+
t_minus=ss_deck.get_by_alias(ALIAS_S_MINUS),
|
199
|
+
)
|
200
|
+
idx = wu.t.get_human_friendly_index()
|
201
|
+
if idx:
|
202
|
+
wu.synthesis.add_indexes_to_aliases(idx)
|
203
|
+
|
204
|
+
wheel.calculate_score()
|
205
|
+
|
206
|
+
|
207
|
+
async def redefine(
|
208
|
+
self, *, modified_statement_per_alias: Dict[str, str]
|
209
|
+
) -> list[Wheel]:
|
210
|
+
"""
|
211
|
+
We can give component statements by alias, e.g., T1 = "New thesis 1", A2+ = "New positive side of antithesis 2"
|
212
|
+
|
213
|
+
Returns a list of wheels with modified statements (updating the internal state)
|
214
|
+
"""
|
215
|
+
if not self.wheel_permutations:
|
216
|
+
raise ValueError("No wheels have been built yet")
|
217
|
+
if modified_statement_per_alias:
|
218
|
+
wheels: list[Wheel] = []
|
219
|
+
for wheel in self.wheel_permutations:
|
220
|
+
new_wisdom_units: list[WisdomUnit] = []
|
221
|
+
is_dirty = False
|
222
|
+
for wu in wheel.wisdom_units:
|
223
|
+
modifications = {}
|
224
|
+
for field, alias in wu.field_to_alias.items():
|
225
|
+
dc = wu.get(alias)
|
226
|
+
if not dc:
|
227
|
+
continue
|
228
|
+
if dc.alias in modified_statement_per_alias:
|
229
|
+
modifications[field] = modified_statement_per_alias[
|
230
|
+
dc.alias
|
231
|
+
]
|
232
|
+
if modifications:
|
233
|
+
is_dirty = True
|
234
|
+
wu_redefined = await self.reasoner.redefine(
|
235
|
+
original=wu, **modifications
|
236
|
+
)
|
237
|
+
idx = wu.t.get_human_friendly_index()
|
238
|
+
if idx:
|
239
|
+
wu_redefined.add_indexes_to_aliases(idx)
|
240
|
+
else:
|
241
|
+
wu_redefined = wu
|
242
|
+
new_wisdom_units.append(wu_redefined)
|
243
|
+
if not is_dirty:
|
244
|
+
# No modifications were made, so preserve the original wheel
|
245
|
+
wheels.append(wheel)
|
246
|
+
else:
|
247
|
+
# Recalculate cycles
|
248
|
+
analyst = self.__sequencer
|
249
|
+
|
250
|
+
theses: list[str] = []
|
251
|
+
for nwu in new_wisdom_units:
|
252
|
+
if nwu.t.alias.startswith("T"):
|
253
|
+
theses.append(nwu.t.statement)
|
254
|
+
else:
|
255
|
+
theses.append(nwu.a.statement)
|
256
|
+
|
257
|
+
t_cycles: list[Cycle] = await analyst.arrange(theses)
|
258
|
+
# TODO: we should do this for each t_cycle, not the first one only. Refactor
|
259
|
+
t_cycle = t_cycles[0]
|
260
|
+
|
261
|
+
wheel_wisdom_units = []
|
262
|
+
for dc in t_cycle.dialectical_components:
|
263
|
+
for nwu in new_wisdom_units:
|
264
|
+
if dc.alias in nwu.t.alias:
|
265
|
+
wheel_wisdom_units.append(nwu)
|
266
|
+
elif dc.alias in nwu.a.alias:
|
267
|
+
wheel_wisdom_units.append(
|
268
|
+
nwu.swap_segments(mutate=True)
|
269
|
+
)
|
270
|
+
|
271
|
+
cycles: list[Cycle] = await analyst.arrange(wheel_wisdom_units)
|
272
|
+
|
273
|
+
for cycle in cycles:
|
274
|
+
w = Wheel(
|
275
|
+
_rearrange_by_causal_sequence(
|
276
|
+
wheel_wisdom_units, cycle, mutate=False
|
277
|
+
),
|
278
|
+
t_cycle=t_cycle,
|
279
|
+
ta_cycle=cycle,
|
280
|
+
)
|
281
|
+
wheels.append(w)
|
282
|
+
self.__wheels = wheels
|
283
|
+
|
284
|
+
for wheel in self.wheel_permutations:
|
285
|
+
wheel.calculate_score()
|
286
|
+
|
287
|
+
return self.wheel_permutations
|
288
|
+
|
289
|
+
|
290
|
+
def _rearrange_by_causal_sequence(
|
291
|
+
wisdom_units: list[WisdomUnit], cycle: Cycle, mutate: bool = True
|
292
|
+
) -> list[WisdomUnit]:
|
293
|
+
"""
|
294
|
+
We expect the cycle to be on the middle ring where theses and antitheses reside.
|
295
|
+
This way we can swap the wisdom unit oppositions if necessary.
|
296
|
+
"""
|
297
|
+
all_aliases = []
|
298
|
+
if cycle.causality_direction == "clockwise":
|
299
|
+
for dc in cycle.dialectical_components:
|
300
|
+
all_aliases.append(dc.alias)
|
301
|
+
else:
|
302
|
+
for dc in reversed(cycle.dialectical_components):
|
303
|
+
all_aliases.append(dc.alias)
|
304
|
+
|
305
|
+
unique_aliases = dict.fromkeys(all_aliases)
|
306
|
+
|
307
|
+
if len(unique_aliases) != 2 * len(wisdom_units):
|
308
|
+
wu_aliases = [wu.t.alias for wu in wisdom_units] + [
|
309
|
+
wu.a.alias for wu in wisdom_units
|
310
|
+
]
|
311
|
+
raise ValueError(
|
312
|
+
f"Not all aliases are present in the causal sequence. wisdom_unit_aliases={wu_aliases}, cycle_aliases={all_aliases}"
|
313
|
+
)
|
314
|
+
|
315
|
+
wu_sorted = []
|
316
|
+
wu_processed = []
|
317
|
+
for alias in unique_aliases:
|
318
|
+
for wu in wisdom_units:
|
319
|
+
if any(item is wu for item in wu_processed):
|
320
|
+
continue
|
321
|
+
if wu.t.alias == alias:
|
322
|
+
wu_sorted.append(wu)
|
323
|
+
wu_processed.append(wu)
|
324
|
+
break
|
325
|
+
if wu.a.alias == alias:
|
326
|
+
wu_sorted.append(wu.swap_segments(mutate=mutate))
|
327
|
+
wu_processed.append(wu)
|
328
|
+
break
|
329
|
+
|
330
|
+
if len(wu_sorted) != len(wisdom_units):
|
331
|
+
raise ValueError("Not all wisdom units were mapped in the causal sequence")
|
332
|
+
|
333
|
+
if mutate:
|
334
|
+
wisdom_units[:] = wu_sorted
|
335
|
+
return wisdom_units
|
336
|
+
else:
|
337
|
+
return wu_sorted
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
|
4
|
+
def dc_replace(text: str, dialectical_component_name: str, replace_to: str) -> str:
|
5
|
+
"""
|
6
|
+
# 1. **`(?<!\w)`**: Ensures `T-` is not preceded by a word character (prevents matches like `T-something`).
|
7
|
+
# 2. **`(["'\(\[\{]?)`**: Matches an optional opening character like `"`, `'`, `(`, `[`, or `{`. This is captured in group `\1`.
|
8
|
+
# 3. **`T-` **: Matches the literal `T-`.
|
9
|
+
# 4. **`(\s|[\]'"}).,!?:]|$)`**:
|
10
|
+
# - Matches any of the following right after `T-`:
|
11
|
+
# - A space (`\s`) (e.g., `T- something` or `T-`).
|
12
|
+
# - A closing punctuation mark like `]`, `'`, `"`, `)`, `}`.
|
13
|
+
# - Specific punctuation characters: `.`, `,`, `!`, `?`, or `:`.
|
14
|
+
# - The end of the line (`$`).
|
15
|
+
#
|
16
|
+
# - This captures both proper sentence endings (e.g., `T-.`) and cases where punctuation appears mid-sentence (e.g., `T-,` or `T-!`).
|
17
|
+
"""
|
18
|
+
return re.sub(
|
19
|
+
r'(?<!\w)(["\'\(\[\{]?)'
|
20
|
+
rf"{re.escape(dialectical_component_name)}"
|
21
|
+
r"(\s|[\]\'\"\)\},.!?:]|$)",
|
22
|
+
# Replacement pattern (preserves surrounding characters and spaces)
|
23
|
+
r"\1" rf"{replace_to}" r"\2",
|
24
|
+
text,
|
25
|
+
flags=re.VERBOSE,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
def dc_safe_replace(text: str, replacements: dict[str, str]) -> str:
|
30
|
+
result = text
|
31
|
+
# Sort keys by length in descending order to replace longer keys first
|
32
|
+
sorted_keys = sorted(replacements.keys(), key=len, reverse=True)
|
33
|
+
|
34
|
+
# First pass: replace with temporary placeholders
|
35
|
+
for key in sorted_keys:
|
36
|
+
result = dc_replace(result, key, f"_{key}_")
|
37
|
+
|
38
|
+
# Second pass: replace placeholders with final values
|
39
|
+
for key in sorted_keys:
|
40
|
+
result = dc_replace(result, f"_{key}_", replacements[key])
|
41
|
+
|
42
|
+
return result
|
@@ -0,0 +1,15 @@
|
|
1
|
+
def decompose_probability_uniformly(probability: float, num_transitions: int) -> float:
|
2
|
+
"""
|
3
|
+
Decompose probability uniformly across all transitions using nth root
|
4
|
+
|
5
|
+
Args:
|
6
|
+
probability: Overall probability (0.0 to 1.0)
|
7
|
+
num_transitions: Number of transitions
|
8
|
+
|
9
|
+
Returns:
|
10
|
+
Individual transition probability that when multiplied gives cycle_probability
|
11
|
+
"""
|
12
|
+
if num_transitions == 0:
|
13
|
+
return 0.0
|
14
|
+
|
15
|
+
return probability ** (1.0 / num_transitions)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
from mirascope import Messages
|
3
|
+
|
4
|
+
|
5
|
+
def extend_tpl(tpl: Messages.Type, prompt: Messages.Type) -> Messages.Type:
|
6
|
+
if isinstance(prompt, list):
|
7
|
+
tpl.extend(prompt)
|
8
|
+
elif hasattr(prompt, "messages"):
|
9
|
+
tpl.extend(prompt.messages)
|
10
|
+
else:
|
11
|
+
tpl.append(prompt)
|
12
|
+
return tpl
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from statistics import geometric_mean
|
2
|
+
|
3
|
+
|
4
|
+
def gm_with_zeros_and_nones_handled(values: list[float]) -> float | None:
|
5
|
+
"""Return GM of positives; 0.0 if any zero present; None if empty."""
|
6
|
+
has_zero = any(v == 0.0 for v in values)
|
7
|
+
positives = [v for v in values if v is not None and v > 0.0]
|
8
|
+
if not values: # list is empty
|
9
|
+
return None
|
10
|
+
if has_zero:
|
11
|
+
return 0.0
|
12
|
+
return geometric_mean(positives) if positives else 0.0 # if only zeros, GM=0.0
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import asyncio
|
2
|
+
import inspect
|
3
|
+
|
4
|
+
|
5
|
+
def is_async(func=None):
|
6
|
+
try:
|
7
|
+
asyncio.get_running_loop() # Check if an active event loop exists
|
8
|
+
if func:
|
9
|
+
# OPTIONAL: Check if the given function is async
|
10
|
+
return inspect.iscoroutinefunction(func)
|
11
|
+
return True
|
12
|
+
except RuntimeError:
|
13
|
+
return False
|
@@ -0,0 +1,80 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
from typing import Any, Callable, Optional, TypeVar
|
3
|
+
|
4
|
+
import litellm
|
5
|
+
from mirascope import llm
|
6
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
7
|
+
|
8
|
+
from dialectical_framework.brain import Brain
|
9
|
+
from dialectical_framework.protocols.has_brain import HasBrain
|
10
|
+
|
11
|
+
T = TypeVar("T")
|
12
|
+
|
13
|
+
def use_brain(brain: Optional[Brain] = None, **llm_call_kwargs):
|
14
|
+
"""
|
15
|
+
Decorator factory for Mirascope that creates an LLM call using the brain's AI provider and model.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
brain: Optional Brain instance to use. If not provided, will expect 'self' to implement HasBrain protocol
|
19
|
+
**llm_call_kwargs: All keyword arguments to pass to @llm.call, including response_model
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
A decorator that wraps methods to make LLM calls
|
23
|
+
"""
|
24
|
+
|
25
|
+
def decorator(method: Callable[..., Any]) -> Callable[..., T]:
|
26
|
+
@wraps(method)
|
27
|
+
async def wrapper(*args, **kwargs) -> T:
|
28
|
+
target_brain = None
|
29
|
+
if brain is not None:
|
30
|
+
target_brain = brain
|
31
|
+
else:
|
32
|
+
# Expect first argument to be self with HasBrain protocol
|
33
|
+
if not args:
|
34
|
+
raise TypeError(
|
35
|
+
"No arguments provided, no brain specified in decorator, and no brain available from DI container"
|
36
|
+
)
|
37
|
+
|
38
|
+
first_arg = args[0]
|
39
|
+
if isinstance(first_arg, HasBrain) and target_brain is None:
|
40
|
+
target_brain = first_arg.brain
|
41
|
+
else:
|
42
|
+
raise TypeError(
|
43
|
+
f"{first_arg.__class__.__name__} must implement {HasBrain.__name__} protocol, "
|
44
|
+
"pass brain parameter to decorator, or have Brain available in DI container"
|
45
|
+
)
|
46
|
+
|
47
|
+
overridden_ai_provider, overridden_ai_model = target_brain.specification()
|
48
|
+
if overridden_ai_provider == "bedrock":
|
49
|
+
# TODO: with Mirascope v2 async should be possible with bedrock, so we should get rid of fallback to litellm
|
50
|
+
# Issue: https://github.com/boto/botocore/issues/458, fallback to "litellm"
|
51
|
+
overridden_ai_provider, overridden_ai_model = (
|
52
|
+
target_brain.modified_specification(ai_provider="litellm")
|
53
|
+
)
|
54
|
+
|
55
|
+
if overridden_ai_provider == "litellm":
|
56
|
+
# We use LiteLLM just for convenience, the real framework is Mirascope.
|
57
|
+
# So anything related to litellm can be supressed
|
58
|
+
litellm.turn_off_message_logging = True
|
59
|
+
|
60
|
+
# Merge brain specification with all parameters
|
61
|
+
call_params = {
|
62
|
+
"provider": overridden_ai_provider,
|
63
|
+
"model": overridden_ai_model,
|
64
|
+
**llm_call_kwargs, # All parameters including response_model
|
65
|
+
}
|
66
|
+
|
67
|
+
# https://mirascope.com/docs/mirascope/learn/retries
|
68
|
+
@retry(
|
69
|
+
stop=stop_after_attempt(3),
|
70
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
71
|
+
)
|
72
|
+
@llm.call(**call_params)
|
73
|
+
async def _llm_call():
|
74
|
+
return await method(*args, **kwargs)
|
75
|
+
|
76
|
+
return await _llm_call()
|
77
|
+
|
78
|
+
return wrapper
|
79
|
+
|
80
|
+
return decorator
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
from typing import Callable
|
2
|
+
|
3
|
+
from mirascope import Messages, llm, prompt_template
|
4
|
+
from mirascope.integrations.langfuse import with_langfuse
|
5
|
+
from mirascope.llm import CallResponse
|
6
|
+
|
7
|
+
from dialectical_framework.validator.check import Check
|
8
|
+
|
9
|
+
|
10
|
+
def is_yes_parser(resp: CallResponse) -> bool:
|
11
|
+
return "YES" in resp.content[:3].upper()
|
12
|
+
|
13
|
+
|
14
|
+
@prompt_template(
|
15
|
+
"""
|
16
|
+
DIALECTICAL OPPOSITION:
|
17
|
+
|
18
|
+
A dialectical opposition presents the conceptual or functional antithesis of the original statement that creates direct opposition, while potentially still allowing their mutual coexistence. For instance, Love vs. Hate or Indifference; Science vs. Superstition, Faith/Belief; Human-caused Global Warming vs. Natural Cycles.
|
19
|
+
|
20
|
+
Can the statement "{antithesis}" be considered a valid dialectical opposition of statement "{thesis}"?
|
21
|
+
|
22
|
+
Start answering with YES or NO. If NO, then provide a correct example. Explain your answer.
|
23
|
+
"""
|
24
|
+
)
|
25
|
+
def is_valid_opposition(antithesis: str, thesis: str) -> Messages.Type: ...
|
26
|
+
|
27
|
+
|
28
|
+
@prompt_template(
|
29
|
+
"""
|
30
|
+
CONTRADICTORY/SEMANTIC OPPOSITION:
|
31
|
+
|
32
|
+
A contradictory/semantic opposition presents a direct semantic opposition and/or contradiction to the original statement that excludes their mutual coexistence. For instance, Happiness vs. Unhappiness; Truthfulness vs. Lie/Deceptiveness; Dependence vs. Independence
|
33
|
+
|
34
|
+
Can the statement "{opposition}" be considered as a contradictory/semantic opposition of "{statement}"?
|
35
|
+
|
36
|
+
Start answering with YES or NO. If NO, then provide a correct example. Explain your answer.
|
37
|
+
"""
|
38
|
+
)
|
39
|
+
def is_strict_opposition(opposition: str, statement: str) -> Messages.Type: ...
|
40
|
+
|
41
|
+
|
42
|
+
@prompt_template(
|
43
|
+
"""
|
44
|
+
Can the statement "{negative_side}" be considered as an exaggerated (overdeveloped, negative) side or outcome of the statement "{statement}"?
|
45
|
+
|
46
|
+
Start answering with YES or NO. If NO, then provide a correct example. Explain your answer.
|
47
|
+
"""
|
48
|
+
)
|
49
|
+
def is_negative_side(negative_side: str, statement: str) -> Messages.Type: ...
|
50
|
+
|
51
|
+
|
52
|
+
@prompt_template(
|
53
|
+
"""
|
54
|
+
Can the statement "{positive_side}" be considered as a positive (constructive and balanced) side of the statement "{statement}"?
|
55
|
+
|
56
|
+
Start answering with YES or NO. If NO, then provide a correct example. Explain your answer.
|
57
|
+
"""
|
58
|
+
)
|
59
|
+
def is_positive_side(positive_side: str, statement: str) -> Messages.Type: ...
|
60
|
+
|
61
|
+
|
62
|
+
@with_langfuse()
|
63
|
+
def check(
|
64
|
+
func: Callable[[str, str], Messages.Type],
|
65
|
+
reasoner,
|
66
|
+
statement1: str,
|
67
|
+
statement2: str,
|
68
|
+
) -> Check:
|
69
|
+
(provider, model) = reasoner.brain.specification()
|
70
|
+
|
71
|
+
@llm.call(provider=provider, model=model, response_model=Check)
|
72
|
+
def _check() -> Check:
|
73
|
+
return func(statement1, statement2)
|
74
|
+
|
75
|
+
return _check()
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
|
3
|
+
|
4
|
+
class Check(BaseModel):
|
5
|
+
valid: float = Field(
|
6
|
+
...,
|
7
|
+
description="For boolean checks, Yes = 1, No = 0 (or True = 1, False = 0). Otherwise it's a float between 0 and 1.",
|
8
|
+
)
|
9
|
+
explanation: str = Field(
|
10
|
+
...,
|
11
|
+
description="A brief explanation how the check was performed to get the resulting value.",
|
12
|
+
)
|