-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbehavior_engine.py
More file actions
174 lines (154 loc) · 7.4 KB
/
behavior_engine.py
File metadata and controls
174 lines (154 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from logging_setup import get_logger
logger = get_logger(__name__)
import json
class BehaviorState:
"""A simple data class to hold the properties of a single behavioral state."""
def __init__(self, state_data: dict):
self.id = state_data.get("id", "0,0")
self.name = state_data.get("name", "Unknown")
self.expressiveness = state_data.get("expressiveness", 0.5)
self.pacing = state_data.get("pacing", "normal")
self.tone_bias = state_data.get("tone_bias", "neutral")
self.memory_strictness = state_data.get("memory_strictness", "medium")
def __str__(self):
return f"[{self.id}] {self.name}"
def get_instructions(self) -> str:
"""Generates a string of instructions for the LLM based on the state's metadata."""
return (
f"Current Behavior State: {self.name} ({self.id}).\n"
f"- Expressiveness Level: {self.expressiveness * 100}%\n"
f"- Conversational Pacing: {self.pacing}\n"
f"- Dominant Tone: {self.tone_bias}\n"
f"- Adherence to Memory: {self.memory_strictness}"
)
class BehaviorStateMachine:
"""
Manages the 5x4 grid of behavioral states for the JL Engine.
"""
def __init__(self, config_path: str):
try:
with open(config_path, 'r') as f:
config = json.load(f)
self.states = [[BehaviorState(s) for s in row] for row in config["states"]]
self.trigger_mappings = config["trigger_mappings"]
self.rows = config["grid_dimensions"]["rows"]
self.columns = config["grid_dimensions"]["columns"]
except (FileNotFoundError, json.JSONDecodeError, KeyError) as e:
print(f"FATAL: Could not load behavior states from {config_path}: {e}")
self.states = [[BehaviorState({}) for _ in range(4)] for _ in range(5)]
self.trigger_mappings = {}
self.rows = 5
self.columns = 4
# Default state is Engaged-Disciplined
self.current_row = 2
self.current_col = 0
# Advisory gating + blending
self._gating_advice = {"level": "allow", "weight": 0.0, "reason": None}
self._blend_weight = 0.0
self._last_blend = None
def get_current_state(self) -> BehaviorState:
"""Returns the current BehaviorState object."""
return self.states[self.current_row][self.current_col]
def get_current_blend(self) -> dict | None:
"""Return the last computed blend bundle for telemetry."""
return self._last_blend
def set_state_by_coords(self, row: int, col: int):
"""Sets the machine to a specific state using grid coordinates."""
self.current_row = max(0, min(row, self.rows - 1))
self.current_col = max(0, min(col, self.columns - 1))
print(f"[Behavior Engine] State set to: {self.get_current_state()}")
self._compute_blend()
def set_state_by_label(self, label: str):
"""
Sets the machine to the first state matching the given name or id.
Returns True if a match is found, else False.
"""
if not label:
return False
label_lower = label.lower()
for r, row in enumerate(self.states):
for c, state in enumerate(row):
if state.name.lower() == label_lower or state.id.lower() == label_lower:
self.set_state_by_coords(r, c)
return True
print(f"[Behavior Engine] WARN: No state found matching '{label}'.")
return False
def transition_by_trigger(self, trigger: str | None, gait: str, gating_advice: dict | None = None):
"""
Sets the state based on a user trigger, influenced by the current gait.
Gating is advisory: it can bias us toward safer states but not hard-block
(safety filters elsewhere still hard-block).
"""
advice = gating_advice or self._gating_advice or {}
if isinstance(advice, dict):
# normalize any hard blocks into weak, unless explicitly marked safety
level = str(advice.get("level", "allow")).lower()
if level == "block":
level = "weak_block"
is_safety = level == "safety_block"
advice = {
"level": level,
"weight": max(0.0, min(1.0, float(advice.get("weight", 0.0) or 0.0))),
"safety": is_safety,
"reason": advice.get("reason"),
}
else:
advice = {"level": "allow", "weight": 0.0, "reason": None}
if advice.get("level") == "weak_block":
self._gating_advice = advice
else:
self._gating_advice = {"level": "allow", "weight": 0.0, "reason": advice.get("reason")}
if trigger and trigger in self.trigger_mappings:
target_row, target_col = self.trigger_mappings[trigger]
# --- Gait Influence Hook ---
# High-energy gaits can push the state towards higher intensity (rows).
if gait in ["trot", "gallop"]:
target_row = min(self.rows - 1, target_row + 1) # Increase intensity by 1
elif gait == "sprint":
target_row = min(self.rows - 1, target_row + 2) # Increase intensity by 2
elif gait == "idle":
target_row = max(0, target_row - 1) # Decrease intensity by 1
# Advisory gating pulls intensity back toward the neutral attractor
if advice.get("level") == "weak_block":
pull = advice.get("weight", 0.3)
target_row = int(round(target_row * (1 - pull) + 2 * pull))
if advice.get("safety"):
target_row = 1
target_col = 0
self.set_state_by_coords(target_row, target_col)
else:
# If no trigger, default to a neutral state
self.set_state_by_coords(2, 1)
# compute blend after transition (may use gating weight)
self._blend_weight = advice.get("weight", 0.0)
self._compute_blend()
def apply_state_to_memory(self, memory_manager: object):
"""Hook for influencing the memory system."""
current_state = self.get_current_state()
strictness = current_state.memory_strictness
print(f"[Behavior Engine] Memory hook called. Strictness level: '{strictness}'")
# --- internal helpers ---
def _compute_blend(self) -> None:
"""
Build a soft blend between the current state and a stabilizer (Engaged-Loose)
when gating bias is active or blend weight is set.
"""
primary = self.get_current_state()
stabilizer = self.states[2][1] # Engaged-Loose
weight = max(0.0, min(1.0, self._blend_weight))
if weight <= 0.05 or primary is stabilizer:
self._last_blend = {
"primary": {"id": primary.id, "name": primary.name},
"secondary": None,
"weights": (1.0, 0.0),
}
return
# choose a nearby neighbor to mix with the stabilizer for smoother transitions
secondary = stabilizer
if self.current_col > 0:
secondary = self.states[self.current_row][self.current_col - 1]
self._last_blend = {
"primary": {"id": primary.id, "name": primary.name},
"secondary": {"id": secondary.id, "name": secondary.name},
"weights": (round(1.0 - weight, 2), round(weight, 2)),
}