hegelion 0.4.0__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 (43) hide show
  1. hegelion/__init__.py +45 -0
  2. hegelion/core/__init__.py +29 -0
  3. hegelion/core/agent.py +166 -0
  4. hegelion/core/autocoding_state.py +293 -0
  5. hegelion/core/backends.py +442 -0
  6. hegelion/core/cache.py +92 -0
  7. hegelion/core/config.py +276 -0
  8. hegelion/core/core.py +649 -0
  9. hegelion/core/engine.py +865 -0
  10. hegelion/core/logging_utils.py +67 -0
  11. hegelion/core/models.py +293 -0
  12. hegelion/core/parsing.py +271 -0
  13. hegelion/core/personas.py +81 -0
  14. hegelion/core/prompt_autocoding.py +353 -0
  15. hegelion/core/prompt_dialectic.py +414 -0
  16. hegelion/core/prompts.py +127 -0
  17. hegelion/core/schema.py +67 -0
  18. hegelion/core/validation.py +68 -0
  19. hegelion/council.py +254 -0
  20. hegelion/examples_data/__init__.py +6 -0
  21. hegelion/examples_data/glm4_6_examples.jsonl +2 -0
  22. hegelion/judge.py +230 -0
  23. hegelion/mcp/__init__.py +3 -0
  24. hegelion/mcp/server.py +918 -0
  25. hegelion/scripts/hegelion_agent_cli.py +90 -0
  26. hegelion/scripts/hegelion_bench.py +117 -0
  27. hegelion/scripts/hegelion_cli.py +497 -0
  28. hegelion/scripts/hegelion_dataset.py +99 -0
  29. hegelion/scripts/hegelion_eval.py +137 -0
  30. hegelion/scripts/mcp_setup.py +150 -0
  31. hegelion/search_providers.py +151 -0
  32. hegelion/training/__init__.py +7 -0
  33. hegelion/training/datasets.py +123 -0
  34. hegelion/training/generator.py +232 -0
  35. hegelion/training/mlx_scu_trainer.py +379 -0
  36. hegelion/training/mlx_trainer.py +181 -0
  37. hegelion/training/unsloth_trainer.py +136 -0
  38. hegelion-0.4.0.dist-info/METADATA +295 -0
  39. hegelion-0.4.0.dist-info/RECORD +43 -0
  40. hegelion-0.4.0.dist-info/WHEEL +5 -0
  41. hegelion-0.4.0.dist-info/entry_points.txt +8 -0
  42. hegelion-0.4.0.dist-info/licenses/LICENSE +21 -0
  43. hegelion-0.4.0.dist-info/top_level.txt +1 -0
hegelion/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """
2
+ Hegelion: Dialectical Reasoning Harness for LLMs
3
+
4
+ A Python package that generates structured thesis-antithesis-synthesis responses
5
+ using Large Language Models, making reasoning patterns and contradictions explicit.
6
+ """
7
+
8
+ from .core.core import (
9
+ run_dialectic,
10
+ run_benchmark,
11
+ run_dialectic_sync,
12
+ run_benchmark_sync,
13
+ dialectic,
14
+ quickstart,
15
+ dialectic_sync,
16
+ quickstart_sync,
17
+ )
18
+ from .core.models import HegelionResult
19
+ from .training.datasets import export_training_data, to_dpo_dataset, to_instruction_tuning_dataset
20
+ from .core.agent import HegelionAgent
21
+ from .core.autocoding_state import AutocodingState
22
+ from .core.prompt_autocoding import AutocodingPrompt, PromptDrivenAutocoding
23
+
24
+ __version__ = "0.4.0"
25
+ __author__ = "Hegelion Contributors"
26
+
27
+ __all__ = [
28
+ "run_dialectic",
29
+ "run_benchmark",
30
+ "run_dialectic_sync",
31
+ "run_benchmark_sync",
32
+ "dialectic",
33
+ "quickstart",
34
+ "dialectic_sync",
35
+ "quickstart_sync",
36
+ "HegelionResult",
37
+ "HegelionAgent",
38
+ "to_dpo_dataset",
39
+ "to_instruction_tuning_dataset",
40
+ "export_training_data",
41
+ # Autocoding (g3-style coach-player loop)
42
+ "AutocodingState",
43
+ "AutocodingPrompt",
44
+ "PromptDrivenAutocoding",
45
+ ]
@@ -0,0 +1,29 @@
1
+ from .core import (
2
+ run_dialectic,
3
+ run_benchmark,
4
+ run_dialectic_sync,
5
+ run_benchmark_sync,
6
+ dialectic,
7
+ quickstart,
8
+ dialectic_sync,
9
+ quickstart_sync,
10
+ )
11
+ from .models import HegelionResult
12
+ from .agent import HegelionAgent
13
+ from .config import get_config, set_config_value, ConfigurationError
14
+
15
+ __all__ = [
16
+ "run_dialectic",
17
+ "run_benchmark",
18
+ "run_dialectic_sync",
19
+ "run_benchmark_sync",
20
+ "dialectic",
21
+ "quickstart",
22
+ "dialectic_sync",
23
+ "quickstart_sync",
24
+ "HegelionResult",
25
+ "HegelionAgent",
26
+ "get_config",
27
+ "set_config_value",
28
+ "ConfigurationError",
29
+ ]
hegelion/core/agent.py ADDED
@@ -0,0 +1,166 @@
1
+ """Agent helpers for using Hegelion inside reflexive/action loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, List, Optional, Union
7
+
8
+ from .backends import LLMBackend
9
+ from .core import run_dialectic, run_dialectic_sync
10
+ from .models import HegelionResult
11
+ from .personas import Persona
12
+
13
+ # Extract the actionable move from a synthesized answer.
14
+ ActionExtractor = Callable[[HegelionResult], str]
15
+
16
+
17
+ def default_action_extractor(result: HegelionResult) -> str:
18
+ """
19
+ Pull a concrete action line from the synthesis when present.
20
+
21
+ Falls back to returning the entire synthesis when no explicit action is
22
+ detected. This keeps the agent usable with existing prompts while still
23
+ preferring concise commands when the model provides them.
24
+ """
25
+
26
+ for line in result.synthesis.splitlines():
27
+ lowered = line.strip().lower()
28
+ if lowered.startswith(("action:", "next action:", "next_action:", "do:", "action ->")):
29
+ # Preserve the original casing of the action line for readability.
30
+ return line.split(":", 1)[1].strip() if ":" in line else line.strip()
31
+ return result.synthesis.strip()
32
+
33
+
34
+ @dataclass
35
+ class AgentStep:
36
+ """Container for a single agent turn."""
37
+
38
+ observation: str
39
+ result: HegelionResult
40
+ action: str
41
+
42
+ def to_dict(self) -> dict:
43
+ """Serialize to a plain dict (handy for logging)."""
44
+
45
+ return {
46
+ "observation": self.observation,
47
+ "action": self.action,
48
+ "result": self.result.to_dict(),
49
+ }
50
+
51
+
52
+ class HegelionAgent:
53
+ """
54
+ Lightweight wrapper that runs the dialectic before acting.
55
+
56
+ Useful for Reflexion-style agents: the agent critiques its own plan
57
+ (thesis → antithesis) and only acts on the synthesized recommendation.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ goal: Optional[str] = None,
64
+ backend: Optional[LLMBackend] = None,
65
+ model: Optional[str] = None,
66
+ personas: Optional[Union[List[Persona], str]] = None,
67
+ iterations: int = 1,
68
+ use_search: bool = False,
69
+ debug: bool = False,
70
+ action_extractor: Optional[ActionExtractor] = None,
71
+ action_guidance: Optional[str] = None,
72
+ ) -> None:
73
+ self.goal = goal
74
+ self.backend = backend
75
+ self.model = model
76
+ self.personas = personas
77
+ self.iterations = iterations
78
+ self.use_search = use_search
79
+ self.debug = debug
80
+ self._action_extractor = action_extractor or default_action_extractor
81
+ self.action_guidance = action_guidance
82
+ self.history: List[AgentStep] = []
83
+
84
+ @classmethod
85
+ def for_coding(
86
+ cls,
87
+ goal: Optional[str] = None,
88
+ **kwargs,
89
+ ) -> "HegelionAgent":
90
+ """
91
+ Convenience constructor tuned for coding agents.
92
+
93
+ Adds guidance that nudges the dialectic toward concrete edits, tests, and
94
+ verification steps to reduce hallucinations in code suggestions.
95
+ """
96
+
97
+ guidance = (
98
+ "Focus on code changes, tests, and reproducible commands. Prefer minimal"
99
+ " diffs, name exact files, and include validation steps. Reject actions"
100
+ " that rely on unverified APIs or assumptions."
101
+ )
102
+ return cls(goal=goal, action_guidance=guidance, **kwargs)
103
+
104
+ def _build_query(self, observation: str) -> str:
105
+ """Shape the agent observation into a dialectic-friendly query."""
106
+
107
+ parts = []
108
+ if self.goal:
109
+ parts.append(f"Goal: {self.goal}")
110
+ parts.append(f"Observation: {observation}")
111
+ if self.action_guidance:
112
+ parts.append(f"Context: {self.action_guidance}")
113
+ parts.append(
114
+ "Run a full thesis → antithesis → synthesis pass on the next step."
115
+ " The antithesis must adversarially attack hallucinations, unverifiable"
116
+ " claims, and risky assumptions. The synthesis should propose a single"
117
+ " concrete, testable action that survives critique and lists any checks"
118
+ " needed to de-risk it. Return the action first, then the reasoning."
119
+ )
120
+ return "\n\n".join(parts)
121
+
122
+ async def deliberate(self, observation: str) -> HegelionResult:
123
+ """Run the full dialectic for an observation and return the result."""
124
+
125
+ query = self._build_query(observation)
126
+ return await run_dialectic(
127
+ query,
128
+ debug=self.debug,
129
+ backend=self.backend,
130
+ model=self.model,
131
+ personas=self.personas,
132
+ iterations=self.iterations,
133
+ use_search=self.use_search,
134
+ )
135
+
136
+ async def act(self, observation: str) -> AgentStep:
137
+ """Deliberate, extract an action, record it, and return the step."""
138
+
139
+ result = await self.deliberate(observation)
140
+ action = self._action_extractor(result)
141
+ step = AgentStep(observation=observation, result=result, action=action)
142
+ self.history.append(step)
143
+ return step
144
+
145
+ def act_sync(self, observation: str) -> AgentStep:
146
+ """Synchronous convenience wrapper around ``act``."""
147
+
148
+ query = self._build_query(observation)
149
+ result = run_dialectic_sync(
150
+ query,
151
+ debug=self.debug,
152
+ backend=self.backend,
153
+ model=self.model,
154
+ personas=self.personas,
155
+ iterations=self.iterations,
156
+ use_search=self.use_search,
157
+ )
158
+ action = self._action_extractor(result)
159
+ step = AgentStep(observation=observation, result=result, action=action)
160
+ self.history.append(step)
161
+ return step
162
+
163
+ def reset_history(self) -> None:
164
+ """Clear stored agent turns."""
165
+
166
+ self.history.clear()
@@ -0,0 +1,293 @@
1
+ """State management for dialectical autocoding sessions.
2
+
3
+ This module provides stateless state management for the coach-player
4
+ autocoding loop based on the g3 paper's adversarial cooperation paradigm.
5
+ State is passed explicitly between tool calls to maintain fresh context each turn.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+
17
+ @dataclass
18
+ class AutocodingState:
19
+ """State for a dialectical autocoding session.
20
+
21
+ This state is passed explicitly between tool calls, enabling fresh
22
+ context each turn while maintaining session continuity.
23
+
24
+ Attributes:
25
+ session_id: Unique identifier for this autocoding session.
26
+ requirements: The requirements document (source of truth).
27
+ current_turn: Current turn number (0-indexed).
28
+ max_turns: Maximum turns before timeout.
29
+ phase: Current phase - init | player | coach | approved | timeout.
30
+ status: Session status - active | approved | rejected | timeout.
31
+ turn_history: List of turn records with feedback and scores.
32
+ last_coach_feedback: Most recent coach feedback for player context.
33
+ quality_scores: List of compliance scores from each coach turn.
34
+ approval_threshold: Minimum score threshold for approval (0-1).
35
+ """
36
+
37
+ session_id: str
38
+ requirements: str
39
+ current_turn: int = 0
40
+ max_turns: int = 10
41
+ phase: str = "init"
42
+ status: str = "active"
43
+ turn_history: List[Dict[str, Any]] = field(default_factory=list)
44
+ last_coach_feedback: Optional[str] = None
45
+ quality_scores: List[float] = field(default_factory=list)
46
+ approval_threshold: float = 0.9
47
+
48
+ def __post_init__(self) -> None:
49
+ """Validate state after initialization."""
50
+ valid_phases = {"init", "player", "coach", "approved", "timeout"}
51
+ valid_statuses = {"active", "approved", "rejected", "timeout"}
52
+
53
+ if self.phase not in valid_phases:
54
+ raise ValueError(f"Invalid phase: {self.phase}. Must be one of {valid_phases}")
55
+ if self.status not in valid_statuses:
56
+ raise ValueError(f"Invalid status: {self.status}. Must be one of {valid_statuses}")
57
+ if not 0 <= self.approval_threshold <= 1:
58
+ raise ValueError(f"approval_threshold must be 0-1, got {self.approval_threshold}")
59
+
60
+ @classmethod
61
+ def create(
62
+ cls,
63
+ requirements: str,
64
+ max_turns: int = 10,
65
+ approval_threshold: float = 0.9,
66
+ ) -> "AutocodingState":
67
+ """Create a new autocoding session.
68
+
69
+ Args:
70
+ requirements: The requirements document (source of truth).
71
+ max_turns: Maximum turns before timeout.
72
+ approval_threshold: Minimum score threshold for approval.
73
+
74
+ Returns:
75
+ A new AutocodingState ready for the first player turn.
76
+ """
77
+ return cls(
78
+ session_id=str(uuid.uuid4()),
79
+ requirements=requirements,
80
+ max_turns=max_turns,
81
+ approval_threshold=approval_threshold,
82
+ phase="player", # Start with player phase
83
+ status="active",
84
+ )
85
+
86
+ def to_dict(self) -> Dict[str, Any]:
87
+ """Serialize state to a dictionary for MCP transport.
88
+
89
+ Returns:
90
+ Dictionary representation of the state.
91
+ """
92
+ return {
93
+ "session_id": self.session_id,
94
+ "requirements": self.requirements,
95
+ "current_turn": self.current_turn,
96
+ "max_turns": self.max_turns,
97
+ "phase": self.phase,
98
+ "status": self.status,
99
+ "turn_history": self.turn_history,
100
+ "last_coach_feedback": self.last_coach_feedback,
101
+ "quality_scores": self.quality_scores,
102
+ "approval_threshold": self.approval_threshold,
103
+ }
104
+
105
+ @classmethod
106
+ def from_dict(cls, data: Dict[str, Any]) -> "AutocodingState":
107
+ """Deserialize state from a dictionary.
108
+
109
+ Args:
110
+ data: Dictionary representation of the state.
111
+
112
+ Returns:
113
+ Reconstructed AutocodingState.
114
+ """
115
+ return cls(
116
+ session_id=data["session_id"],
117
+ requirements=data["requirements"],
118
+ current_turn=data.get("current_turn", 0),
119
+ max_turns=data.get("max_turns", 10),
120
+ phase=data.get("phase", "init"),
121
+ status=data.get("status", "active"),
122
+ turn_history=data.get("turn_history", []),
123
+ last_coach_feedback=data.get("last_coach_feedback"),
124
+ quality_scores=data.get("quality_scores", []),
125
+ approval_threshold=data.get("approval_threshold", 0.9),
126
+ )
127
+
128
+ def advance_to_coach(self) -> "AutocodingState":
129
+ """Advance state from player phase to coach phase.
130
+
131
+ Returns:
132
+ New state with coach phase active.
133
+
134
+ Raises:
135
+ ValueError: If not in player phase or session not active.
136
+ """
137
+ if self.phase != "player":
138
+ raise ValueError(f"Cannot advance to coach from phase: {self.phase}")
139
+ if self.status != "active":
140
+ raise ValueError(f"Cannot advance: session status is {self.status}")
141
+
142
+ return AutocodingState(
143
+ session_id=self.session_id,
144
+ requirements=self.requirements,
145
+ current_turn=self.current_turn,
146
+ max_turns=self.max_turns,
147
+ phase="coach",
148
+ status="active",
149
+ turn_history=self.turn_history.copy(),
150
+ last_coach_feedback=self.last_coach_feedback,
151
+ quality_scores=self.quality_scores.copy(),
152
+ approval_threshold=self.approval_threshold,
153
+ )
154
+
155
+ def advance_turn(
156
+ self,
157
+ coach_feedback: str,
158
+ approved: bool,
159
+ compliance_score: Optional[float] = None,
160
+ ) -> "AutocodingState":
161
+ """Advance state after coach review.
162
+
163
+ Args:
164
+ coach_feedback: Feedback from the coach agent.
165
+ approved: Whether the coach approved the implementation.
166
+ compliance_score: Optional compliance score (0-1).
167
+
168
+ Returns:
169
+ New state with updated turn, feedback, and status.
170
+ """
171
+ if self.phase != "coach":
172
+ raise ValueError(f"Cannot advance turn from phase: {self.phase}")
173
+
174
+ new_turn = self.current_turn + 1
175
+ new_history = self.turn_history.copy()
176
+ new_scores = self.quality_scores.copy()
177
+
178
+ # Record turn history
179
+ turn_record = {
180
+ "turn": self.current_turn,
181
+ "feedback": coach_feedback,
182
+ "approved": approved,
183
+ "score": compliance_score,
184
+ }
185
+ new_history.append(turn_record)
186
+
187
+ if compliance_score is not None:
188
+ new_scores.append(compliance_score)
189
+
190
+ # Determine next phase and status
191
+ if approved:
192
+ new_phase = "approved"
193
+ new_status = "approved"
194
+ elif new_turn >= self.max_turns:
195
+ new_phase = "timeout"
196
+ new_status = "timeout"
197
+ else:
198
+ new_phase = "player"
199
+ new_status = "active"
200
+
201
+ return AutocodingState(
202
+ session_id=self.session_id,
203
+ requirements=self.requirements,
204
+ current_turn=new_turn,
205
+ max_turns=self.max_turns,
206
+ phase=new_phase,
207
+ status=new_status,
208
+ turn_history=new_history,
209
+ last_coach_feedback=coach_feedback,
210
+ quality_scores=new_scores,
211
+ approval_threshold=self.approval_threshold,
212
+ )
213
+
214
+ def is_complete(self) -> bool:
215
+ """Check if the session has completed (approved or timeout).
216
+
217
+ Returns:
218
+ True if session is no longer active.
219
+ """
220
+ return self.status in {"approved", "rejected", "timeout"}
221
+
222
+ def turns_remaining(self) -> int:
223
+ """Get the number of turns remaining.
224
+
225
+ Returns:
226
+ Number of turns left before timeout.
227
+ """
228
+ return max(0, self.max_turns - self.current_turn)
229
+
230
+ def average_score(self) -> Optional[float]:
231
+ """Calculate average compliance score across turns.
232
+
233
+ Returns:
234
+ Average score, or None if no scores recorded.
235
+ """
236
+ if not self.quality_scores:
237
+ return None
238
+ return sum(self.quality_scores) / len(self.quality_scores)
239
+
240
+ def summary(self) -> str:
241
+ """Generate a human-readable summary of session state.
242
+
243
+ Returns:
244
+ Summary string for display.
245
+ """
246
+ avg_score = self.average_score()
247
+ score_str = f"{avg_score:.1%}" if avg_score is not None else "N/A"
248
+
249
+ return (
250
+ f"Session: {self.session_id[:8]}...\n"
251
+ f"Turn: {self.current_turn + 1}/{self.max_turns}\n"
252
+ f"Phase: {self.phase}\n"
253
+ f"Status: {self.status}\n"
254
+ f"Avg Score: {score_str}"
255
+ )
256
+
257
+
258
+ def save_session(state: AutocodingState, filepath: str) -> None:
259
+ """Save an autocoding session to a JSON file.
260
+
261
+ Args:
262
+ state: The AutocodingState to save.
263
+ filepath: Path to save the session JSON file.
264
+ """
265
+ path = Path(filepath)
266
+ path.parent.mkdir(parents=True, exist_ok=True)
267
+
268
+ with open(path, "w") as f:
269
+ json.dump(state.to_dict(), f, indent=2)
270
+
271
+
272
+ def load_session(filepath: str) -> AutocodingState:
273
+ """Load an autocoding session from a JSON file.
274
+
275
+ Args:
276
+ filepath: Path to the session JSON file to load.
277
+
278
+ Returns:
279
+ Reconstructed AutocodingState.
280
+
281
+ Raises:
282
+ FileNotFoundError: If the session file doesn't exist.
283
+ json.JSONDecodeError: If the file is not valid JSON.
284
+ ValueError: If the JSON doesn't contain valid session data.
285
+ """
286
+ path = Path(filepath)
287
+ if not path.exists():
288
+ raise FileNotFoundError(f"Session file not found: {filepath}")
289
+
290
+ with open(path, "r") as f:
291
+ data = json.load(f)
292
+
293
+ return AutocodingState.from_dict(data)