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,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,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
+ )