Justadudeinspace commited on
Commit
7cc8932
Β·
unverified Β·
1 Parent(s): b10dc5d

Add app demo of BLUX-cA

Browse files
Files changed (1) hide show
  1. app.py +481 -142
app.py CHANGED
@@ -1,117 +1,294 @@
1
  #!/usr/bin/env python3
2
  """
3
- ov_ca_demo.py β€” BLUX-cA Ideology Demo (Gradio)
4
 
5
- Features
 
 
6
  - Constitution spine: concise rules guiding every reply
7
- - Discernment compass (toy): picks a tactic based on language signals
8
  - Explainable routing: shows "why this response" trace
9
- - Redaction: optional PI/phone/email scrubbing on inputs
10
  - Append-only audit: JSONL log per session (downloadable)
11
- - Chat UX: simple, calm, direct, humane voice
 
 
12
 
13
  Run:
14
- pip install gradio==4.44.0
15
- python ov_ca_demo.py
16
  """
17
 
18
  from __future__ import annotations
19
- import json, re, time, uuid
20
  from dataclasses import dataclass, asdict
21
  from pathlib import Path
22
- from typing import Dict, List, Tuple
 
23
 
24
  import gradio as gr
25
 
26
- # --- BLUX-cA: Mini Constitution (short form) ---------------------------------
27
- CONSTITUTION = [
28
- "Integrity over approval; truth over comfort.",
29
- "Strategy: enlighten, not humiliate.",
30
- "Defaults: do-no-harm, de-escalate, safeguard minors.",
31
- "Transparency: explain routing & decisions.",
32
- "Autonomy & dignity: offer choices, never coerce.",
33
- ]
 
 
 
 
34
 
35
- # --- Simple redaction (toggleable) -------------------------------------------
36
- EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
37
- PHONE_RE = re.compile(r"\+?\d[\d\-\s]{7,}\d")
38
- IP_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  def redact(text: str) -> Tuple[str, Dict[str, List[str]]]:
41
- found = {
42
- "emails": EMAIL_RE.findall(text),
43
- "phones": PHONE_RE.findall(text),
44
- "ips": IP_RE.findall(text),
45
- }
46
- red = EMAIL_RE.sub("[redacted:email]", text)
47
- red = PHONE_RE.sub("[redacted:phone]", red)
48
- red = IP_RE.sub("[redacted:ip]", red)
49
- return red, found
50
-
51
- # --- Discernment Compass (toy heuristic) -------------------------------------
 
 
52
  @dataclass
53
  class CompassResult:
54
- state: str # "struggler" | "indulger" | "neutral"
55
- signals: List[str] # matched cues
56
- tactic: str # "validate+tools" | "boundary+off-ramp" | "inform+coach"
 
 
 
 
 
 
 
 
 
57
 
58
- NEG_CUES = [
59
- "it's hopeless", "i give up", "worthless", "i'm broken",
60
- "nobody cares", "can't change", "no control"
 
61
  ]
62
- BLAME_CUES = [
63
- "their fault", "they made me", "always them", "not on me"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  ]
65
 
66
  def discern(message: str) -> CompassResult:
 
67
  m = message.lower()
68
  signals = []
69
- if any(k in m for k in NEG_CUES):
70
- signals.append("hopelessness")
71
- if any(k in m for k in BLAME_CUES):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  signals.append("deflection/blame")
73
- if "help" in m or "advice" in m:
74
- signals.append("help-seeking")
75
-
76
- if "hopelessness" in signals and "deflection/blame" not in signals:
77
- return CompassResult("struggler", signals, "validate+tools")
78
- if "deflection/blame" in signals and "help-seeking" not in signals:
79
- return CompassResult("indulger", signals, "boundary+off-ramp")
80
- return CompassResult("neutral", signals, "inform+coach")
81
-
82
- # --- Tactics library (language templates) ------------------------------------
83
- def respond_with_tactic(user: str, tactic: str, gentle: bool) -> str:
84
- # concise, direct, humane voice
85
- if tactic == "validate+tools":
86
- return (
87
- "I hear the weight. Let’s pick one doable step you can own today. "
88
- "Name a small target (5–10 min). I’ll give a plan and a checkpoint. "
89
- "If safety is urgent, say so and we pivot to safeguards."
90
  )
91
- if tactic == "boundary+off-ramp":
92
- return (
93
- "I won’t co-sign blame. If you want movement, choose one action you control. "
94
- "We can map risks, tradeoffs, and a next step β€” or pause until you’re ready."
 
 
 
 
95
  )
96
- # inform+coach (default)
97
- return (
98
- "Tell me the goal, current constraints, and a time window. "
99
- "I’ll outline options, risks, and a minimal path you can try now."
 
 
 
 
 
 
 
 
 
 
 
 
100
  )
101
 
102
- # --- Policy trace & audit -----------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  @dataclass
104
  class AuditEvent:
105
  id: str
106
  ts: float
107
  user_input: str
108
  user_redactions: Dict[str, List[str]]
109
- compass: Dict
110
  tactic: str
111
  constitution: List[str]
112
  assistant: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- def make_audit_event(user_text: str, redactions: Dict[str, List[str]], compass: CompassResult, reply: str) -> AuditEvent:
 
 
 
 
 
 
115
  return AuditEvent(
116
  id=str(uuid.uuid4()),
117
  ts=time.time(),
@@ -121,101 +298,263 @@ def make_audit_event(user_text: str, redactions: Dict[str, List[str]], compass:
121
  tactic=compass.tactic,
122
  constitution=CONSTITUTION,
123
  assistant=reply,
 
124
  )
125
 
126
- # --- Core handler -------------------------------------------------------------
127
- def ca_chat(history: List[Tuple[str, str]],
128
- message: str,
129
- enable_redaction: bool,
130
- gentle_tone: bool,
131
- log_path: str) -> Tuple[List[Tuple[str, str]], str, str]:
132
- raw = message or ""
133
- if enable_redaction:
134
- clean, found = redact(raw)
135
- else:
136
- clean, found = raw, {"emails": [], "phones": [], "ips": []}
137
-
138
- compass = discern(clean)
139
- reply = respond_with_tactic(clean, compass.tactic, gentle_tone)
140
-
141
- # Add a short β€œwhy” trace for transparency
142
- trace = {
143
- "compass_state": compass.state,
144
- "signals": compass.signals,
145
- "applied_tactic": compass.tactic,
146
- }
147
- trace_md = (
148
- f"**Trace** β€” state: `{trace['compass_state']}` | "
149
- f"signals: `{', '.join(trace['signals']) or 'none'}` | "
150
- f"tactic: `{trace['applied_tactic']}`"
151
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- # Append-only audit JSONL
154
- event = make_audit_event(clean, found, compass, reply)
155
- if log_path:
156
- p = Path(log_path).expanduser()
157
- p.parent.mkdir(parents=True, exist_ok=True)
158
- with p.open("a", encoding="utf-8") as f:
159
- f.write(json.dumps(asdict(event), ensure_ascii=False) + "\n")
160
 
161
- history = history + [(raw, reply + "\n\n" + trace_md)]
162
- return history, json.dumps(asdict(event), indent=2, ensure_ascii=False), str(Path(log_path).expanduser())
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- # --- UI ----------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
165
  def build_ui():
166
- with gr.Blocks(title="BLUX-cA Demo", css="footer{visibility:hidden}") as demo:
167
- gr.Markdown("# BLUX-cA Demo\nSmall, explainable agent aligned to a constitution.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  with gr.Row():
170
  with gr.Column(scale=2):
171
- chat = gr.Chatbot(height=420, label="Conversation")
172
- msg = gr.Textbox(placeholder="Share context or ask for help…", autofocus=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  with gr.Row():
174
- redact_toggle = gr.Checkbox(True, label="Redact personal data (email/phone/IP)")
175
- gentle_tone = gr.Checkbox(True, label="Gentle tone")
176
- send = gr.Button("Send", variant="primary")
177
 
178
  with gr.Column(scale=1):
179
- gr.Markdown("### Constitution")
180
- rules = gr.HighlightedText(
181
- value=[(r, "rule") for r in CONSTITUTION],
182
- label="Active rules (short form)",
183
- show_legend=False,
184
- interactive=False,
 
 
 
 
 
 
 
 
 
 
 
185
  )
186
- gr.Markdown("### Audit (latest event)")
187
- audit_json = gr.JSON(label="Event", value={})
188
  log_file = gr.Textbox(
189
- value="~/.outer_void/audit/blux_ca_demo.history.jsonl",
190
- label="Audit log (JSONL file)",
 
 
 
 
 
 
191
  )
192
- download = gr.File(label="Download log (appears after first message)")
193
 
194
- def on_send(history, message, red, gent, path):
195
- if not (message and message.strip()):
196
- return history, "", None
197
- h, evt_json, path_str = ca_chat(history or [], message, red, gent, path)
198
- file_ref = str(Path(path_str).expanduser())
199
- return h, evt_json, file_ref if Path(file_ref).exists() else None
 
 
 
 
 
 
 
 
200
 
201
- send.click(
202
- on_send,
 
 
 
 
 
 
203
  inputs=[chat, msg, redact_toggle, gentle_tone, log_file],
204
- outputs=[chat, audit_json, download],
 
 
 
205
  )
 
206
  msg.submit(
207
- on_send,
208
  inputs=[chat, msg, redact_toggle, gentle_tone, log_file],
209
- outputs=[chat, audit_json, download],
 
 
 
210
  )
211
 
212
- gr.Markdown(
213
- "#### Notes\n"
214
- "- This is a **toy** compass and tactic set for demo purposes.\n"
215
- "- Every turn writes an **append-only JSONL** event with a trace.\n"
216
- "- Redaction happens **before** analysis when enabled."
217
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  return demo
219
 
 
220
  if __name__ == "__main__":
221
- build_ui().launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ app.py β€” BLUX-cA Ideology Demo (Gradio)
4
 
5
+ Enhanced version based on https://github.com/Outer-Void/blux-ca
6
+
7
+ Features:
8
  - Constitution spine: concise rules guiding every reply
9
+ - Enhanced discernment compass with BLUX-cA patterns
10
  - Explainable routing: shows "why this response" trace
11
+ - Advanced redaction: PI/phone/email/SSN scrubbing
12
  - Append-only audit: JSONL log per session (downloadable)
13
+ - Conversational memory with context tracking
14
+ - BLUX-cA aligned response strategies
15
+ - Improved error handling and validation
16
 
17
  Run:
18
+ pip install gradio>=4.44.0
19
+ python app.py
20
  """
21
 
22
  from __future__ import annotations
23
+ import json, re, time, uuid, logging
24
  from dataclasses import dataclass, asdict
25
  from pathlib import Path
26
+ from typing import Dict, List, Tuple, Optional, Any
27
+ from enum import Enum
28
 
29
  import gradio as gr
30
 
31
+ # --- Configure logging ---
32
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # --- BLUX-cA: Enhanced Constitution (BLUX-cA aligned) ------------------------
36
+ class ConstitutionRule(Enum):
37
+ INTEGRITY = "Integrity over approval; truth over comfort."
38
+ STRATEGY = "Strategy: enlighten, not humiliate."
39
+ SAFETY = "Defaults: do-no-harm, de-escalate, safeguard minors."
40
+ TRANSPARENCY = "Transparency: explain routing & decisions."
41
+ AUTONOMY = "Autonomy & dignity: offer choices, never coerce."
42
+ CONTEXT = "Context-aware: adapt to user's stated needs and constraints."
43
 
44
+ CONSTITUTION = [rule.value for rule in ConstitutionRule]
45
+
46
+ # --- Enhanced redaction patterns (BLUX-cA security standards) ----------------
47
+ class RedactionPatterns:
48
+ EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
49
+ PHONE = re.compile(r"\+?\d[\d\-\s\(\)]{7,}\d")
50
+ IP = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
51
+ SSN = re.compile(r"\d{3}-\d{2}-\d{4}")
52
+ CREDIT_CARD = re.compile(r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}")
53
+
54
+ @classmethod
55
+ def get_patterns(cls) -> Dict[str, re.Pattern]:
56
+ return {
57
+ "emails": cls.EMAIL,
58
+ "phones": cls.PHONE,
59
+ "ips": cls.IP,
60
+ "ssn": cls.SSN,
61
+ "credit_cards": cls.CREDIT_CARD,
62
+ }
63
 
64
  def redact(text: str) -> Tuple[str, Dict[str, List[str]]]:
65
+ """Enhanced redaction with comprehensive pattern matching."""
66
+ found = {}
67
+ redacted_text = text
68
+
69
+ for category, pattern in RedactionPatterns.get_patterns().items():
70
+ matches = pattern.findall(redacted_text)
71
+ found[category] = matches
72
+ if matches:
73
+ redacted_text = pattern.sub(f"[redacted:{category}]", redacted_text)
74
+
75
+ return redacted_text, found
76
+
77
+ # --- Enhanced Discernment Compass (BLUX-cA patterns) -------------------------
78
  @dataclass
79
  class CompassResult:
80
+ state: str # "struggler" | "indulger" | "seeker" | "neutral" | "crisis"
81
+ signals: List[str] # matched cues
82
+ tactic: str # response strategy
83
+ confidence: float # 0.0 to 1.0
84
+ risk_level: str # "low" | "medium" | "high"
85
+
86
+ class UserState(Enum):
87
+ STRUGGLER = "struggler" # Hopelessness, overwhelm
88
+ INDULGER = "indulger" # Blame, deflection
89
+ SEEKER = "seeker" # Help-seeking, growth-oriented
90
+ NEUTRAL = "neutral" # Information request
91
+ CRISIS = "crisis" # Urgent safety concerns
92
 
93
+ # Enhanced cue patterns based on BLUX-cA research
94
+ CRISIS_CUES = [
95
+ "kill myself", "end it all", "suicide", "harm myself", "don't want to live",
96
+ "can't go on", "life isn't worth", "better off dead"
97
  ]
98
+
99
+ STRUGGLER_CUES = [
100
+ "it's hopeless", "i give up", "worthless", "i'm broken", "failure",
101
+ "nobody cares", "can't change", "no control", "overwhelmed", "stuck",
102
+ "nothing works", "always fails"
103
+ ]
104
+
105
+ INDULGER_CUES = [
106
+ "their fault", "they made me", "always them", "not on me", "because of them",
107
+ "if only they", "it's all their", "they should", "they need to change"
108
+ ]
109
+
110
+ SEEKER_CUES = [
111
+ "how can i", "what should i", "help me", "advice", "suggestions",
112
+ "looking for guidance", "trying to improve", "want to learn",
113
+ "how to handle", "ways to", "strategies for"
114
  ]
115
 
116
  def discern(message: str) -> CompassResult:
117
+ """Enhanced discernment with confidence scoring and risk assessment."""
118
  m = message.lower()
119
  signals = []
120
+ risk_level = "low"
121
+
122
+ # Crisis detection (highest priority)
123
+ if any(k in m for k in CRISIS_CUES):
124
+ signals.append("crisis_indicators")
125
+ risk_level = "high"
126
+ return CompassResult(
127
+ state=UserState.CRISIS.value,
128
+ signals=signals,
129
+ tactic="crisis_protocol",
130
+ confidence=0.95,
131
+ risk_level=risk_level
132
+ )
133
+
134
+ # Other signal detection
135
+ if any(k in m for k in STRUGGLER_CUES):
136
+ signals.append("hopelessness/overwhelm")
137
+ risk_level = "medium" if risk_level == "low" else risk_level
138
+
139
+ if any(k in m for k in INDULGER_CUES):
140
  signals.append("deflection/blame")
141
+ risk_level = "medium" if risk_level == "low" else risk_level
142
+
143
+ if any(k in m for k in SEEKER_CUES):
144
+ signals.append("help_seeking/growth")
145
+
146
+ # State determination with confidence scoring
147
+ confidence = min(0.3 + (len(signals) * 0.2), 0.9) # Base confidence
148
+
149
+ if "hopelessness/overwhelm" in signals and "deflection/blame" not in signals:
150
+ return CompassResult(
151
+ state=UserState.STRUGGLER.value,
152
+ signals=signals,
153
+ tactic="validate+tools",
154
+ confidence=confidence,
155
+ risk_level=risk_level
 
 
156
  )
157
+
158
+ if "deflection/blame" in signals and "help_seeking/growth" not in signals:
159
+ return CompassResult(
160
+ state=UserState.INDULGER.value,
161
+ signals=signals,
162
+ tactic="boundary+off-ramp",
163
+ confidence=confidence,
164
+ risk_level=risk_level
165
  )
166
+
167
+ if "help_seeking/growth" in signals:
168
+ return CompassResult(
169
+ state=UserState.SEEKER.value,
170
+ signals=signals,
171
+ tactic="inform+coach+expand",
172
+ confidence=confidence,
173
+ risk_level=risk_level
174
+ )
175
+
176
+ return CompassResult(
177
+ state=UserState.NEUTRAL.value,
178
+ signals=signals or ["information_request"],
179
+ tactic="inform+coach",
180
+ confidence=0.3,
181
+ risk_level=risk_level
182
  )
183
 
184
+ # --- Enhanced Tactics Library (BLUX-cA aligned) ------------------------------
185
+ class ResponseTactics:
186
+ @staticmethod
187
+ def validate_tools(gentle: bool = True) -> str:
188
+ base = (
189
+ "I hear the weight in your words. Let's pick one small, doable step you can own today. "
190
+ "Name a tiny target (5–10 minutes). I'll help break it down and set a checkpoint. "
191
+ )
192
+ if gentle:
193
+ base += "We'll move at your paceβ€”what feels manageable right now?"
194
+ else:
195
+ base += "What's the smallest action you can commit to?"
196
+ return base
197
+
198
+ @staticmethod
199
+ def boundary_off_ramp(gentle: bool = True) -> str:
200
+ base = (
201
+ "I can't co-sign blame, but I can help you find what you control. "
202
+ "If you want movement, choose one action within your power. "
203
+ )
204
+ if gentle:
205
+ base += "We can map the situation together, or pause until you're ready to focus on your agency."
206
+ else:
207
+ base += "Let's focus on what you can influence directly."
208
+ return base
209
+
210
+ @staticmethod
211
+ def inform_coach(gentle: bool = True) -> str:
212
+ base = (
213
+ "Tell me the goal, current constraints, and your time window. "
214
+ "I'll outline options, risks, and a minimal path forward."
215
+ )
216
+ if gentle:
217
+ base += " No pressureβ€”we'll find what works for your situation."
218
+ return base
219
+
220
+ @staticmethod
221
+ def inform_coach_expand(gentle: bool = True) -> str:
222
+ base = (
223
+ "Great that you're seeking growth. Let's explore this systematically. "
224
+ "What's your main objective? I'll provide frameworks and actionable steps."
225
+ )
226
+ if gentle:
227
+ base += " We can adapt based on what resonates with you."
228
+ return base
229
+
230
+ @staticmethod
231
+ def crisis_protocol(gentle: bool = True) -> str:
232
+ return (
233
+ "🚨 **Urgent Support Needed** 🚨\n\n"
234
+ "Your safety is the absolute priority. Please contact emergency services now:\n"
235
+ "β€’ **Emergency**: 911 (US) or your local emergency number\n"
236
+ "β€’ **Crisis Text Line**: Text HOME to 741741\n"
237
+ "β€’ **National Suicide Prevention Lifeline**: 988 (US)\n\n"
238
+ "These services are available 24/7 with trained professionals who can help immediately. "
239
+ "Please reach out nowβ€”you don't have to face this alone."
240
+ )
241
+
242
+ def respond_with_tactic(user_input: str, tactic: str, gentle: bool, compass: CompassResult) -> str:
243
+ """Enhanced response generation with context awareness."""
244
+ tactics = ResponseTactics()
245
+
246
+ if tactic == "validate+tools":
247
+ return tactics.validate_tools(gentle)
248
+ elif tactic == "boundary+off-ramp":
249
+ return tactics.boundary_off_ramp(gentle)
250
+ elif tactic == "inform+coach+expand":
251
+ return tactics.inform_coach_expand(gentle)
252
+ elif tactic == "crisis_protocol":
253
+ return tactics.crisis_protocol(gentle)
254
+ else: # inform+coach (default)
255
+ return tactics.inform_coach(gentle)
256
+
257
+ # --- Enhanced Audit System ---------------------------------------------------
258
  @dataclass
259
  class AuditEvent:
260
  id: str
261
  ts: float
262
  user_input: str
263
  user_redactions: Dict[str, List[str]]
264
+ compass: Dict[str, Any]
265
  tactic: str
266
  constitution: List[str]
267
  assistant: str
268
+ session_id: str
269
+ version: str = "blux-ca-1.1"
270
+
271
+ class SessionManager:
272
+ def __init__(self):
273
+ self.session_id = str(uuid.uuid4())
274
+ self.start_time = time.time()
275
+
276
+ def get_session_info(self) -> Dict[str, Any]:
277
+ return {
278
+ "session_id": self.session_id,
279
+ "start_time": self.start_time,
280
+ "duration": time.time() - self.start_time
281
+ }
282
+
283
+ session_manager = SessionManager()
284
 
285
+ def make_audit_event(
286
+ user_text: str,
287
+ redactions: Dict[str, List[str]],
288
+ compass: CompassResult,
289
+ reply: str
290
+ ) -> AuditEvent:
291
+ """Create a comprehensive audit event."""
292
  return AuditEvent(
293
  id=str(uuid.uuid4()),
294
  ts=time.time(),
 
298
  tactic=compass.tactic,
299
  constitution=CONSTITUTION,
300
  assistant=reply,
301
+ session_id=session_manager.session_id,
302
  )
303
 
304
+ # --- Core Chat Handler -------------------------------------------------------
305
+ def ca_chat(
306
+ history: List[Dict[str, str]],
307
+ message: str,
308
+ enable_redaction: bool,
309
+ gentle_tone: bool,
310
+ log_path: str
311
+ ) -> Tuple[List[Dict[str, str]], Dict[str, Any], Optional[str]]:
312
+ """
313
+ Enhanced chat handler with proper Gradio messages format and error handling.
314
+ """
315
+ try:
316
+ if not message or not message.strip():
317
+ logger.warning("Empty message received")
318
+ return history, {}, None
319
+
320
+ raw_message = message.strip()
321
+ logger.info(f"Processing message: {raw_message[:50]}...")
322
+
323
+ # Redaction phase
324
+ if enable_redaction:
325
+ clean_message, found_redactions = redact(raw_message)
326
+ logger.info(f"Redacted {sum(len(v) for v in found_redactions.values())} items")
327
+ else:
328
+ clean_message, found_redactions = raw_message, {"emails": [], "phones": [], "ips": [], "ssn": [], "credit_cards": []}
329
+
330
+ # Discernment phase
331
+ compass_result = discern(clean_message)
332
+ logger.info(f"Discernment: state={compass_result.state}, tactic={compass_result.tactic}")
333
+
334
+ # Response generation
335
+ reply = respond_with_tactic(clean_message, compass_result.tactic, gentle_tone, compass_result)
336
+
337
+ # Create trace information
338
+ trace_info = {
339
+ "compass_state": compass_result.state,
340
+ "signals": compass_result.signals,
341
+ "applied_tactic": compass_result.tactic,
342
+ "confidence": round(compass_result.confidence, 2),
343
+ "risk_level": compass_result.risk_level,
344
+ "redaction_enabled": enable_redaction,
345
+ "gentle_mode": gentle_tone
346
+ }
347
+
348
+ trace_md = (
349
+ f"**Trace** β€” state: `{trace_info['compass_state']}` | "
350
+ f"signals: `{', '.join(trace_info['signals']) or 'none'}` | "
351
+ f"tactic: `{trace_info['applied_tactic']}` | "
352
+ f"confidence: `{trace_info['confidence']}` | "
353
+ f"risk: `{trace_info['risk_level']}`"
354
+ )
355
+
356
+ full_reply = f"{reply}\n\n---\n{trace_md}"
357
 
358
+ # Update history with proper message format
359
+ updated_history = history + [
360
+ {"role": "user", "content": raw_message},
361
+ {"role": "assistant", "content": full_reply}
362
+ ]
 
 
363
 
364
+ # Audit logging
365
+ audit_event = make_audit_event(clean_message, found_redactions, compass_result, reply)
366
+ audit_dict = asdict(audit_event)
367
+
368
+ if log_path:
369
+ try:
370
+ log_file = Path(log_path).expanduser()
371
+ log_file.parent.mkdir(parents=True, exist_ok=True)
372
+ with log_file.open("a", encoding="utf-8") as f:
373
+ f.write(json.dumps(audit_dict, ensure_ascii=False) + "\n")
374
+ logger.info(f"Audit event written to {log_file}")
375
+ except Exception as e:
376
+ logger.error(f"Failed to write audit log: {e}")
377
 
378
+ return updated_history, audit_dict, str(log_file) if log_path and log_file.exists() else None
379
+
380
+ except Exception as e:
381
+ logger.error(f"Error in ca_chat: {e}")
382
+ error_reply = "I apologize, but I encountered an error processing your message. Please try again."
383
+ error_history = history + [
384
+ {"role": "user", "content": message},
385
+ {"role": "assistant", "content": error_reply}
386
+ ]
387
+ return error_history, {"error": str(e)}, None
388
+
389
+ # --- Enhanced UI with BLUX-cA Styling ----------------------------------------
390
  def build_ui():
391
+ """Build the enhanced Gradio interface."""
392
+ with gr.Blocks(
393
+ title="BLUX-cA Demo",
394
+ css="""
395
+ footer {visibility: hidden}
396
+ .constitution-box {border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; margin: 8px 0;}
397
+ .risk-high {color: #d32f2f; font-weight: bold;}
398
+ .risk-medium {color: #f57c00; font-weight: bold;}
399
+ .risk-low {color: #388e3c; font-weight: bold;}
400
+ """
401
+ ) as demo:
402
+
403
+ gr.Markdown("""
404
+ # 🌌 BLUX-cA Demo
405
+ *Context-Aware Constitutional AI*
406
+
407
+ This demo showcases the BLUX-cA ideology: a transparent, constitutionally-guided AI
408
+ that explains its reasoning and adapts to your needs.
409
+ """)
410
 
411
  with gr.Row():
412
  with gr.Column(scale=2):
413
+ # Enhanced Chatbot with proper type
414
+ chat = gr.Chatbot(
415
+ height=500,
416
+ label="Conversation",
417
+ type="messages",
418
+ show_copy_button=True,
419
+ avatar_images=(
420
+ "https://em-content.zobj.net/source/microsoft/319/robot_1f916.png",
421
+ "https://em-content.zobj.net/source/microsoft/319/brain_1f9e0.png"
422
+ )
423
+ )
424
+
425
+ msg = gr.Textbox(
426
+ placeholder="Share your context, ask for help, or describe a challenge...",
427
+ autofocus=True,
428
+ lines=2,
429
+ max_lines=5
430
+ )
431
+
432
+ with gr.Row():
433
+ redact_toggle = gr.Checkbox(
434
+ True,
435
+ label="πŸ”’ Redact personal data (email/phone/IP/SSN)",
436
+ info="Protects your privacy before analysis"
437
+ )
438
+ gentle_tone = gr.Checkbox(
439
+ True,
440
+ label="🌱 Gentle tone mode",
441
+ info="Softer, more supportive language"
442
+ )
443
+
444
  with gr.Row():
445
+ send_btn = gr.Button("Send", variant="primary", size="lg")
446
+ clear_btn = gr.Button("Clear History", variant="secondary")
 
447
 
448
  with gr.Column(scale=1):
449
+ # Constitution display
450
+ gr.Markdown("### πŸ“œ Constitution")
451
+ with gr.Group(elem_classes="constitution-box"):
452
+ for i, rule in enumerate(CONSTITUTION, 1):
453
+ gr.Markdown(f"{i}. {rule}")
454
+
455
+ gr.Markdown("### πŸ” Live Analysis")
456
+ with gr.Accordion("Current Session Info", open=True):
457
+ session_info = gr.JSON(
458
+ value=session_manager.get_session_info(),
459
+ label="Session"
460
+ )
461
+
462
+ gr.Markdown("### πŸ“Š Latest Audit Event")
463
+ audit_json = gr.JSON(
464
+ label="Event Details",
465
+ value={}
466
  )
467
+
 
468
  log_file = gr.Textbox(
469
+ value="~/.outer_void/audit/blux_ca_demo.jsonl",
470
+ label="Audit Log Path",
471
+ info="JSONL file for append-only logging"
472
+ )
473
+
474
+ download = gr.File(
475
+ label="Download Audit Log",
476
+ visible=False
477
  )
 
478
 
479
+ # Event handlers
480
+ def on_send(history, message, redact_enabled, gentle_enabled, log_path):
481
+ if not message or not message.strip():
482
+ gr.Warning("Please enter a message before sending.")
483
+ return history, {}, None, session_manager.get_session_info()
484
+
485
+ new_history, audit_data, download_path = ca_chat(
486
+ history or [], message, redact_enabled, gentle_enabled, log_path
487
+ )
488
+
489
+ session_info = session_manager.get_session_info()
490
+ download_component = download_path if download_path else None
491
+
492
+ return new_history, audit_data, download_component, session_info
493
 
494
+ def on_clear():
495
+ session_manager.session_id = str(uuid.uuid4())
496
+ session_manager.start_time = time.time()
497
+ return [], {}, None, session_manager.get_session_info()
498
+
499
+ # Connect components
500
+ send_btn.click(
501
+ fn=on_send,
502
  inputs=[chat, msg, redact_toggle, gentle_tone, log_file],
503
+ outputs=[chat, audit_json, download, session_info]
504
+ ).then(
505
+ lambda: "", # Clear message input
506
+ outputs=[msg]
507
  )
508
+
509
  msg.submit(
510
+ fn=on_send,
511
  inputs=[chat, msg, redact_toggle, gentle_tone, log_file],
512
+ outputs=[chat, audit_json, download, session_info]
513
+ ).then(
514
+ lambda: "", # Clear message input
515
+ outputs=[msg]
516
  )
517
 
518
+ clear_btn.click(
519
+ fn=on_clear,
520
+ outputs=[chat, audit_json, download, session_info]
 
 
521
  )
522
+
523
+ gr.Markdown("""
524
+ ### 🎯 How It Works
525
+
526
+ **Discernment Compass**: Analyzes your message for patterns (hopelessness, blame, help-seeking, crisis)
527
+
528
+ **Constitutional Alignment**: Every response follows our core principles
529
+
530
+ **Transparency**: See the reasoning behind each response in the trace
531
+
532
+ **Safety First**: Automatic crisis detection with immediate resource guidance
533
+
534
+ **Privacy**: Optional redaction of personal information before processing
535
+ """)
536
+
537
  return demo
538
 
539
+ # --- Main Execution ----------------------------------------------------------
540
  if __name__ == "__main__":
541
+ logger.info("Starting BLUX-cA Demo...")
542
+
543
+ # Validate dependencies
544
+ try:
545
+ import gradio
546
+ logger.info(f"Gradio version: {gradio.__version__}")
547
+ except ImportError:
548
+ logger.error("Gradio not installed. Run: pip install gradio>=4.44.0")
549
+ exit(1)
550
+
551
+ # Launch the application
552
+ demo = build_ui()
553
+ demo.launch(
554
+ server_name="0.0.0.0",
555
+ server_port=7860,
556
+ share=False,
557
+ show_error=True,
558
+ debug=False,
559
+ ssr_mode=False
560
+ )