healspace 0.2.0__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: healspace
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: HealSpace — Guardian AI for Hugging Face Spaces
5
5
  Author: Onyxl · TeraBites
6
6
  License: Apache-2.0
@@ -0,0 +1,43 @@
1
+ """
2
+ HealSpace — Guardian AI for Hugging Face Spaces
3
+ ================================================
4
+ Automatically detects backend errors, heals common issues,
5
+ and protects your Space with a branded lineage seal.
6
+
7
+ Quick start — one line in app.py
8
+ ---------------------------------
9
+ import gradio as gr
10
+ from healspace import heal
11
+
12
+ with gr.Blocks() as demo:
13
+ gr.Markdown("# My Space")
14
+ # ... your UI ...
15
+
16
+ heal(demo) # wraps, protects, and launches
17
+ demo.launch()
18
+
19
+ Mini AI code healer (context-aware)
20
+ -------------------------------------
21
+ from healspace.brain import CodeHealer
22
+
23
+ healer = CodeHealer()
24
+ patch = healer.heal_traceback(traceback_text, source_file="app.py")
25
+ print(patch) # shows a diff
26
+ patch.apply() # writes fix back to file
27
+ """
28
+
29
+ from .core import HealSpace, heal, diagnose, fix
30
+ from .errors import KnownError, ErrorReport
31
+ from .brain import CodeHealer, CodePatch, StyleProfile
32
+
33
+ __version__ = "0.3.0"
34
+ __author__ = "Onyxl · TeraBites"
35
+ __license__ = "Apache 2.0"
36
+ __all__ = [
37
+ # guardian
38
+ "HealSpace", "heal", "diagnose", "fix",
39
+ # error types
40
+ "KnownError", "ErrorReport",
41
+ # mini AI
42
+ "CodeHealer", "CodePatch", "StyleProfile",
43
+ ]
@@ -0,0 +1,692 @@
1
+ """
2
+ healspace.brain — HealSpace Mini AI
3
+ =====================================
4
+ A lightweight, context-aware code healer.
5
+
6
+ How it works
7
+ ------------
8
+ 1. **Understand** — reads the lines BEFORE and AFTER the error to learn:
9
+ - indentation style (tabs vs spaces, width)
10
+ - naming convention (snake_case, camelCase, etc.)
11
+ - quote style (' vs ")
12
+ - whether type hints are used
13
+ - comment style (# inline, docstrings, etc.)
14
+
15
+ 2. **Diagnose** — matches the error against known patterns (from errors.py)
16
+ and extracts the exact line + file from the traceback.
17
+
18
+ 3. **Fix** — generates a drop-in replacement that:
19
+ - uses the SAME style as surrounding code
20
+ - is minimal (touches only what's broken)
21
+ - includes a short inline comment explaining the fix
22
+
23
+ 4. **Apply** (optional) — writes the fix back to the source file in-place.
24
+
25
+ Public API
26
+ ----------
27
+ from healspace.brain import CodeHealer
28
+
29
+ healer = CodeHealer()
30
+ patch = healer.heal_traceback(traceback_text, source_file="app.py")
31
+ print(patch.fixed_line)
32
+ patch.apply() # writes fix to file
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ import ast
38
+ import textwrap
39
+ from dataclasses import dataclass, field
40
+ from pathlib import Path
41
+ from typing import Optional, List, Dict, Tuple
42
+
43
+ from .errors import KnownError, ErrorReport
44
+
45
+
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # Style fingerprint
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ @dataclass
51
+ class StyleProfile:
52
+ """Describes the coding style of a block of source code."""
53
+ indent_char: str = " " # " " or "\t"
54
+ indent_width: int = 4 # spaces per level (irrelevant if tabs)
55
+ quote_char: str = '"' # dominant quote character
56
+ uses_type_hints:bool = False
57
+ naming: str = "snake" # "snake" | "camel" | "pascal" | "mixed"
58
+ trailing_comma: bool = False
59
+ max_line_len: int = 88
60
+
61
+ def indent(self, level: int = 1) -> str:
62
+ if self.indent_char == "\t":
63
+ return "\t" * level
64
+ return " " * (self.indent_width * level)
65
+
66
+ def q(self, s: str) -> str:
67
+ """Wrap string literal in the dominant quote style."""
68
+ return f"{self.quote_char}{s}{self.quote_char}"
69
+
70
+
71
+ def _detect_style(lines: List[str]) -> StyleProfile:
72
+ """Infer StyleProfile from a list of source lines."""
73
+ profile = StyleProfile()
74
+
75
+ # ── Indentation ──────────────────────────────────────────────────────────
76
+ tab_lines = sum(1 for l in lines if l.startswith("\t"))
77
+ space_lines = sum(1 for l in lines if l.startswith(" "))
78
+ if tab_lines > space_lines:
79
+ profile.indent_char = "\t"
80
+ profile.indent_width = 1
81
+ else:
82
+ profile.indent_char = " "
83
+ # detect width from the smallest non-zero indent
84
+ widths = []
85
+ for l in lines:
86
+ stripped = l.lstrip(" ")
87
+ w = len(l) - len(stripped)
88
+ if w > 0:
89
+ widths.append(w)
90
+ if widths:
91
+ profile.indent_width = min(widths)
92
+
93
+ # ── Quote style ──────────────────────────────────────────────────────────
94
+ singles = sum(l.count("'") for l in lines)
95
+ doubles = sum(l.count('"') for l in lines)
96
+ profile.quote_char = "'" if singles > doubles else '"'
97
+
98
+ # ── Type hints ───────────────────────────────────────────────────────────
99
+ profile.uses_type_hints = any(
100
+ re.search(r"def \w+\(.*:.*\) ->|: int|: str|: bool|: float|: list|: dict",
101
+ l) for l in lines
102
+ )
103
+
104
+ # ── Naming convention ────────────────────────────────────────────────────
105
+ names = re.findall(r"\b([a-zA-Z_]\w*)\b", " ".join(lines))
106
+ snake = sum(1 for n in names if "_" in n and n == n.lower())
107
+ camel = sum(1 for n in names if re.match(r"[a-z][a-zA-Z0-9]*[A-Z]", n))
108
+ pascal = sum(1 for n in names if re.match(r"[A-Z][a-z]", n))
109
+ if snake >= camel and snake >= pascal:
110
+ profile.naming = "snake"
111
+ elif camel > pascal:
112
+ profile.naming = "camel"
113
+ else:
114
+ profile.naming = "pascal"
115
+
116
+ # ── Trailing commas ──────────────────────────────────────────────────────
117
+ profile.trailing_comma = any(re.search(r",\s*[\)\]\}]", l) for l in lines)
118
+
119
+ # ── Max line length ──────────────────────────────────────────────────────
120
+ lens = [len(l.rstrip()) for l in lines if l.strip()]
121
+ if lens:
122
+ profile.max_line_len = max(79, min(120, max(lens)))
123
+
124
+ return profile
125
+
126
+
127
+ # ─────────────────────────────────────────────────────────────────────────────
128
+ # Fix generators — one per error type
129
+ # ─────────────────────────────────────────────────────────────────────────────
130
+
131
+ def _fix_missing_dep(
132
+ report: ErrorReport,
133
+ broken_line: str,
134
+ style: StyleProfile,
135
+ context_before: List[str],
136
+ ) -> str:
137
+ """
138
+ ModuleNotFoundError → add a try/except import that falls back gracefully,
139
+ matching the surrounding import block style.
140
+ """
141
+ pkg = report.title.split(": ", 1)[-1].strip()
142
+ q = style.quote_char
143
+
144
+ # Is the broken line a plain `import X` or `from X import Y`?
145
+ m_from = re.match(r"^(\s*)(from\s+\S+\s+import\s+.+)$", broken_line)
146
+ m_import = re.match(r"^(\s*)(import\s+\S+.*)$", broken_line)
147
+
148
+ if m_from:
149
+ ind, stmt = m_from.group(1), m_from.group(2)
150
+ return (
151
+ f"{ind}try:\n"
152
+ f"{ind} {stmt}\n"
153
+ f"{ind}except ImportError:\n"
154
+ f"{ind} raise ImportError(\n"
155
+ f"{ind} {q}HealSpace: {pkg!r} is missing. \"\n"
156
+ f"{ind} \"Add {q}{pkg}{q} to requirements.txt and restart the Space.{q}\n"
157
+ f"{ind} )\n"
158
+ )
159
+ elif m_import:
160
+ ind, stmt = m_import.group(1), m_import.group(2)
161
+ alias = re.sub(r"import\s+(\S+).*", r"\1", stmt).split(".")[0]
162
+ return (
163
+ f"{ind}try:\n"
164
+ f"{ind} {stmt}\n"
165
+ f"{ind}except ImportError:\n"
166
+ f"{ind} {alias} = None "
167
+ f"# HealSpace: install {q}{pkg}{q} to enable this feature\n"
168
+ )
169
+ else:
170
+ ind = re.match(r"^(\s*)", broken_line).group(1)
171
+ return (
172
+ f"{ind}# HealSpace fix: '{pkg}' not found — "
173
+ f"add it to requirements.txt\n"
174
+ f"{broken_line}"
175
+ )
176
+
177
+
178
+ def _fix_port_conflict(
179
+ report: ErrorReport,
180
+ broken_line: str,
181
+ style: StyleProfile,
182
+ context_before: List[str],
183
+ ) -> str:
184
+ """
185
+ Port already in use → add server_port with a fallback scan,
186
+ or wrap launch() with a retry loop matching existing style.
187
+ """
188
+ ind = re.match(r"^(\s*)", broken_line).group(1)
189
+ q = style.quote_char
190
+
191
+ # If the broken line is a .launch() call, patch it
192
+ if "launch" in broken_line:
193
+ # Extract existing kwargs
194
+ has_share = "share" in broken_line
195
+ share_kw = ", share=True" if has_share else ""
196
+ return (
197
+ f"{ind}# HealSpace fix: auto-find a free port if 7860 is taken\n"
198
+ f"{ind}import socket as _hs_sock\n"
199
+ f"{ind}def _hs_free_port(start=7860):\n"
200
+ f"{ind} for p in range(start, start + 20):\n"
201
+ f"{ind} try:\n"
202
+ f"{ind} _hs_sock.socket().bind(({q}0.0.0.0{q}, p)); return p\n"
203
+ f"{ind} except OSError:\n"
204
+ f"{ind} continue\n"
205
+ f"{ind} return start\n"
206
+ f"{ind}demo.launch(server_port=_hs_free_port(){share_kw})\n"
207
+ )
208
+ return (
209
+ f"{ind}# HealSpace fix: port conflict — restart Space or change server_port\n"
210
+ f"{broken_line}"
211
+ )
212
+
213
+
214
+ def _fix_gpu_oom(
215
+ report: ErrorReport,
216
+ broken_line: str,
217
+ style: StyleProfile,
218
+ context_before: List[str],
219
+ ) -> str:
220
+ """CUDA OOM → wrap the offending call with empty_cache + half-precision hint."""
221
+ ind = re.match(r"^(\s*)", broken_line).group(1)
222
+ return (
223
+ f"{ind}# HealSpace fix: clear CUDA cache before this call\n"
224
+ f"{ind}try:\n"
225
+ f"{ind} import torch as _hs_torch; _hs_torch.cuda.empty_cache()\n"
226
+ f"{ind}except Exception:\n"
227
+ f"{ind} pass\n"
228
+ f"{broken_line}"
229
+ )
230
+
231
+
232
+ def _fix_syntax_error(
233
+ report: ErrorReport,
234
+ broken_line: str,
235
+ style: StyleProfile,
236
+ context_before: List[str],
237
+ ) -> str:
238
+ """
239
+ SyntaxError → attempt common automatic repairs:
240
+ - unmatched brackets
241
+ - missing colon after def/class/if/for/while/with/else/elif/except/finally
242
+ - obvious string not closed
243
+ """
244
+ line = broken_line.rstrip()
245
+ ind = re.match(r"^(\s*)", line).group(1)
246
+ fixed = line
247
+
248
+ # Missing colon
249
+ if re.match(
250
+ r"^\s*(def |class |if |elif |else|for |while |with |try|except|finally)",
251
+ line
252
+ ) and not line.rstrip().endswith(":"):
253
+ fixed = line.rstrip() + ":"
254
+
255
+ # Unbalanced brackets — add missing closers
256
+ opens = {"(": ")", "[": "]", "{": "}"}
257
+ stack = []
258
+ in_str = False
259
+ str_ch = None
260
+ for ch in fixed:
261
+ if in_str:
262
+ if ch == str_ch:
263
+ in_str = False
264
+ else:
265
+ if ch in ('"', "'"):
266
+ in_str = True; str_ch = ch
267
+ elif ch in opens:
268
+ stack.append(opens[ch])
269
+ elif ch in opens.values() and stack and stack[-1] == ch:
270
+ stack.pop()
271
+ if stack:
272
+ fixed = fixed.rstrip() + "".join(reversed(stack))
273
+
274
+ if fixed != line:
275
+ return (
276
+ f"{ind}# HealSpace fix: auto-corrected syntax\n"
277
+ f"{fixed}\n"
278
+ )
279
+ # Can't auto-fix — leave with comment
280
+ return (
281
+ f"{ind}# HealSpace: syntax error here — check brackets, colons, and quotes\n"
282
+ f"{broken_line}"
283
+ )
284
+
285
+
286
+ def _fix_auth_error(
287
+ report: ErrorReport,
288
+ broken_line: str,
289
+ style: StyleProfile,
290
+ context_before: List[str],
291
+ ) -> str:
292
+ """Auth error → inject HF_TOKEN env lookup before the offending call."""
293
+ ind = re.match(r"^(\s*)", broken_line).group(1)
294
+ q = style.quote_char
295
+ return (
296
+ f"{ind}# HealSpace fix: ensure HF_TOKEN is set as a Space secret\n"
297
+ f"{ind}import os as _hs_os\n"
298
+ f"{ind}if not _hs_os.environ.get({q}HF_TOKEN{q}):\n"
299
+ f"{ind} raise EnvironmentError(\n"
300
+ f"{ind} {q}HealSpace: Add HF_TOKEN as a secret in Space Settings.{q}\n"
301
+ f"{ind} )\n"
302
+ f"{broken_line}"
303
+ )
304
+
305
+
306
+ def _fix_runtime_error(
307
+ report: ErrorReport,
308
+ broken_line: str,
309
+ style: StyleProfile,
310
+ context_before: List[str],
311
+ ) -> str:
312
+ """Generic RuntimeError → wrap in try/except with descriptive re-raise."""
313
+ ind = re.match(r"^(\s*)", broken_line).group(1)
314
+ q = style.quote_char
315
+ desc = report.description[:80].replace(q, "\\" + q)
316
+ return (
317
+ f"{ind}# HealSpace fix: guarded against RuntimeError\n"
318
+ f"{ind}try:\n"
319
+ f"{ind} {broken_line.strip()}\n"
320
+ f"{ind}except RuntimeError as _hs_e:\n"
321
+ f"{ind} print(f{q}[HealSpace] RuntimeError caught: {{_hs_e}}{q})\n"
322
+ f"{ind} raise\n"
323
+ )
324
+
325
+
326
+ _FIX_GENERATORS: Dict[str, callable] = {
327
+ "missing_dep": _fix_missing_dep,
328
+ "port_conflict": _fix_port_conflict,
329
+ "gpu_oom": _fix_gpu_oom,
330
+ "syntax_error": _fix_syntax_error,
331
+ "auth_error": _fix_auth_error,
332
+ "runtime_error": _fix_runtime_error,
333
+ }
334
+
335
+ # fallback for error types without a dedicated generator
336
+ def _fix_generic(
337
+ report: ErrorReport,
338
+ broken_line: str,
339
+ style: StyleProfile,
340
+ context_before: List[str],
341
+ ) -> str:
342
+ ind = re.match(r"^(\s*)", broken_line).group(1)
343
+ return (
344
+ f"{ind}# HealSpace: {report.title}\n"
345
+ f"{ind}# Suggested: {report.fixes[0] if report.fixes else 'see traceback'}\n"
346
+ f"{broken_line}"
347
+ )
348
+
349
+
350
+ # ─────────────────────────────────────────────────────────────────────────────
351
+ # Patch dataclass
352
+ # ─────────────────────────────────────────────────────────────────────────────
353
+
354
+ @dataclass
355
+ class CodePatch:
356
+ """
357
+ A ready-to-apply code fix produced by CodeHealer.
358
+
359
+ Attributes
360
+ ----------
361
+ source_file : path to the file that was healed (may be None)
362
+ error_line : 1-based line number of the broken line
363
+ original : the original broken line(s)
364
+ fixed : the replacement code (may span multiple lines)
365
+ report : the ErrorReport that triggered this fix
366
+ style : the StyleProfile inferred from surrounding code
367
+ context_before : lines above the error (for display)
368
+ context_after : lines below the error (for display)
369
+ """
370
+ source_file: Optional[str]
371
+ error_line: int
372
+ original: str
373
+ fixed: str
374
+ report: ErrorReport
375
+ style: StyleProfile
376
+ context_before: List[str] = field(default_factory=list)
377
+ context_after: List[str] = field(default_factory=list)
378
+
379
+ # ── Display ───────────────────────────────────────────────────────────
380
+
381
+ def diff(self) -> str:
382
+ """Return a human-readable unified-diff-style patch."""
383
+ sep = "─" * 60
384
+ lines = [
385
+ sep,
386
+ f" HealSpace AI — {self.report.title}",
387
+ f" File: {self.source_file or '(unknown)'} line {self.error_line}",
388
+ sep,
389
+ "",
390
+ ]
391
+ for l in self.context_before[-3:]:
392
+ lines.append(f" {l.rstrip()}")
393
+ for l in self.original.splitlines():
394
+ lines.append(f"- {l.rstrip()}")
395
+ for l in self.fixed.splitlines():
396
+ lines.append(f"+ {l.rstrip()}")
397
+ for l in self.context_after[:3]:
398
+ lines.append(f" {l.rstrip()}")
399
+ lines += ["", f" Style: indent={self.style.indent_width}sp "
400
+ f"quotes={self.style.quote_char} "
401
+ f"hints={self.style.uses_type_hints} "
402
+ f"naming={self.style.naming}", sep]
403
+ return "\n".join(lines)
404
+
405
+ def apply(self, dry_run: bool = False) -> bool:
406
+ """
407
+ Write the fix back to *source_file* in-place.
408
+
409
+ Parameters
410
+ ----------
411
+ dry_run : bool
412
+ If True, print what would change but don't write.
413
+
414
+ Returns
415
+ -------
416
+ True if the file was (or would be) modified, False otherwise.
417
+ """
418
+ if not self.source_file:
419
+ print("[HealSpace Brain] No source file — cannot apply patch.")
420
+ return False
421
+
422
+ path = Path(self.source_file)
423
+ if not path.exists():
424
+ print(f"[HealSpace Brain] File not found: {path}")
425
+ return False
426
+
427
+ lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
428
+ idx = self.error_line - 1 # 0-based
429
+ if idx < 0 or idx >= len(lines):
430
+ print(f"[HealSpace Brain] Line {self.error_line} out of range.")
431
+ return False
432
+
433
+ fixed_lines = [
434
+ l if l.endswith("\n") else l + "\n"
435
+ for l in self.fixed.splitlines()
436
+ ]
437
+
438
+ if dry_run:
439
+ print(self.diff())
440
+ print("[HealSpace Brain] (dry run — file not modified)")
441
+ return True
442
+
443
+ lines[idx : idx + 1] = fixed_lines
444
+ path.write_text("".join(lines), encoding="utf-8")
445
+ print(f"[HealSpace Brain] ✅ Patch applied → {path} (line {self.error_line})")
446
+ return True
447
+
448
+ def __str__(self) -> str:
449
+ return self.diff()
450
+
451
+
452
+ # ─────────────────────────────────────────────────────────────────────────────
453
+ # Traceback parser
454
+ # ─────────────────────────────────────────────────────────────────────────────
455
+
456
+ def _parse_traceback(tb: str) -> Tuple[Optional[str], Optional[int], Optional[str]]:
457
+ """
458
+ Extract (filename, lineno, line_text) from a Python traceback string.
459
+ Returns the INNERMOST (deepest) frame, which is the root cause.
460
+ """
461
+ # Matches: File "app.py", line 42, in some_func
462
+ frame_re = re.compile(r'File "([^"]+)", line (\d+)')
463
+ line_re = re.compile(r'^\s{4}(.+)$') # the code line after the frame
464
+
465
+ matches = list(frame_re.finditer(tb))
466
+ if not matches:
467
+ return None, None, None
468
+
469
+ # Take the last frame
470
+ m = matches[-1]
471
+ filename = m.group(1)
472
+ lineno = int(m.group(2))
473
+
474
+ # The line of code is the next non-empty line after the frame header
475
+ after = tb[m.end():]
476
+ code_line = None
477
+ for raw in after.splitlines():
478
+ stripped = raw.strip()
479
+ if stripped and not stripped.startswith("^") and not stripped.startswith("~"):
480
+ code_line = raw
481
+ break
482
+
483
+ return filename, lineno, code_line
484
+
485
+
486
+ # ─────────────────────────────────────────────────────────────────────────────
487
+ # Main healer
488
+ # ─────────────────────────────────────────────────────────────────────────────
489
+
490
+ class CodeHealer:
491
+ """
492
+ HealSpace Mini AI — context-aware code fixer.
493
+
494
+ Parameters
495
+ ----------
496
+ context_window : int
497
+ How many lines above and below the error to read for style analysis
498
+ and context display (default 15).
499
+ auto_apply : bool
500
+ If True, automatically write fixes back to source files (default False).
501
+ verbose : bool
502
+ Print detailed logs (default True).
503
+
504
+ Example
505
+ -------
506
+ >>> from healspace.brain import CodeHealer
507
+ >>> healer = CodeHealer()
508
+ >>> patch = healer.heal_traceback(traceback_text, source_file="app.py")
509
+ >>> print(patch) # diff view
510
+ >>> patch.apply() # write fix to file
511
+ """
512
+
513
+ def __init__(
514
+ self,
515
+ context_window: int = 15,
516
+ auto_apply: bool = False,
517
+ verbose: bool = True,
518
+ ):
519
+ self.context_window = context_window
520
+ self.auto_apply = auto_apply
521
+ self.verbose = verbose
522
+
523
+ # ── Internal helpers ──────────────────────────────────────────────────
524
+
525
+ def _read_context(
526
+ self, filepath: str, lineno: int
527
+ ) -> Tuple[List[str], str, List[str]]:
528
+ """
529
+ Read (lines_before, broken_line, lines_after) from *filepath*.
530
+ Falls back gracefully if the file can't be read.
531
+ """
532
+ try:
533
+ all_lines = Path(filepath).read_text(encoding="utf-8").splitlines()
534
+ except Exception:
535
+ return [], "", []
536
+
537
+ idx = lineno - 1 # 0-based
538
+ before = all_lines[max(0, idx - self.context_window) : idx]
539
+ broken = all_lines[idx] if idx < len(all_lines) else ""
540
+ after = all_lines[idx + 1 : idx + 1 + self.context_window]
541
+ return before, broken, after
542
+
543
+ def _generate_fix(
544
+ self,
545
+ report: ErrorReport,
546
+ broken_line: str,
547
+ style: StyleProfile,
548
+ context_before: List[str],
549
+ ) -> str:
550
+ """Dispatch to the appropriate fix generator."""
551
+ generator = _FIX_GENERATORS.get(report.error_type, _fix_generic)
552
+ return generator(report, broken_line, style, context_before)
553
+
554
+ # ── Public API ────────────────────────────────────────────────────────
555
+
556
+ def heal_traceback(
557
+ self,
558
+ traceback_text: str,
559
+ source_file: Optional[str] = None,
560
+ ) -> Optional[CodePatch]:
561
+ """
562
+ Full healing pipeline:
563
+ parse traceback → diagnose → read context → detect style → generate fix.
564
+
565
+ Parameters
566
+ ----------
567
+ traceback_text : str
568
+ The full Python traceback (as printed to stderr).
569
+ source_file : str, optional
570
+ Override the filename from the traceback (useful in Gradio callbacks
571
+ where the path is /tmp/...).
572
+
573
+ Returns
574
+ -------
575
+ CodePatch if a fix was generated, None if the error was unrecognised.
576
+ """
577
+ if self.verbose:
578
+ print("[HealSpace Brain] 🧠 Analysing traceback…")
579
+
580
+ # 1. Diagnose
581
+ report = KnownError.match(traceback_text)
582
+ if report is None:
583
+ if self.verbose:
584
+ print("[HealSpace Brain] No known error pattern matched.")
585
+ return None
586
+
587
+ if self.verbose:
588
+ print(f"[HealSpace Brain] 🔍 Detected: {report.title}")
589
+
590
+ # 2. Parse traceback for file + line
591
+ tb_file, lineno, tb_code_line = _parse_traceback(traceback_text)
592
+ filepath = source_file or tb_file
593
+ lineno = lineno or 1
594
+
595
+ # 3. Read surrounding code from file
596
+ context_before, broken_line, context_after = [], "", []
597
+ if filepath:
598
+ context_before, broken_line, context_after = self._read_context(
599
+ filepath, lineno
600
+ )
601
+
602
+ # If we couldn't read the file, use the line from the traceback itself
603
+ if not broken_line and tb_code_line:
604
+ broken_line = tb_code_line.strip()
605
+
606
+ if not broken_line:
607
+ broken_line = f"# (line {lineno} — source unavailable)"
608
+
609
+ # 4. Detect style from surrounding code
610
+ all_context = context_before + [broken_line] + context_after
611
+ style = _detect_style(all_context)
612
+
613
+ if self.verbose:
614
+ print(
615
+ f"[HealSpace Brain] 📐 Style: "
616
+ f"indent={style.indent_width}{'tab' if style.indent_char == chr(9) else 'sp'} "
617
+ f"quotes={style.quote_char} "
618
+ f"type_hints={style.uses_type_hints} "
619
+ f"naming={style.naming}"
620
+ )
621
+
622
+ # 5. Generate fix
623
+ fixed = self._generate_fix(report, broken_line, style, context_before)
624
+
625
+ if self.verbose:
626
+ print(f"[HealSpace Brain] 🔧 Fix generated for line {lineno}")
627
+
628
+ patch = CodePatch(
629
+ source_file = filepath,
630
+ error_line = lineno,
631
+ original = broken_line,
632
+ fixed = fixed,
633
+ report = report,
634
+ style = style,
635
+ context_before = context_before,
636
+ context_after = context_after,
637
+ )
638
+
639
+ if self.auto_apply:
640
+ patch.apply()
641
+
642
+ return patch
643
+
644
+ def heal_file(
645
+ self,
646
+ source_file: str,
647
+ traceback_text: str,
648
+ ) -> Optional[CodePatch]:
649
+ """Convenience wrapper — same as heal_traceback with source_file forced."""
650
+ return self.heal_traceback(traceback_text, source_file=source_file)
651
+
652
+ def heal_code(
653
+ self,
654
+ code: str,
655
+ error_text: str,
656
+ lineno: int = 1,
657
+ ) -> Optional[CodePatch]:
658
+ """
659
+ Heal an in-memory code string without a file on disk.
660
+
661
+ Parameters
662
+ ----------
663
+ code : full source code as a string
664
+ error_text : error message or traceback
665
+ lineno : 1-based line number of the broken line
666
+
667
+ Returns
668
+ -------
669
+ CodePatch (source_file=None, apply() will no-op)
670
+ """
671
+ report = KnownError.match(error_text)
672
+ if report is None:
673
+ return None
674
+
675
+ lines = code.splitlines()
676
+ idx = lineno - 1
677
+ broken_line = lines[idx] if 0 <= idx < len(lines) else ""
678
+ before = lines[max(0, idx - self.context_window) : idx]
679
+ after = lines[idx + 1 : idx + 1 + self.context_window]
680
+ style = _detect_style(before + [broken_line] + after)
681
+ fixed = self._generate_fix(report, broken_line, style, before)
682
+
683
+ return CodePatch(
684
+ source_file = None,
685
+ error_line = lineno,
686
+ original = broken_line,
687
+ fixed = fixed,
688
+ report = report,
689
+ style = style,
690
+ context_before = before,
691
+ context_after = after,
692
+ )
@@ -12,9 +12,12 @@ import subprocess
12
12
  from typing import Optional, List, Callable
13
13
 
14
14
  from .errors import KnownError, ErrorReport
15
+ from .brain import CodeHealer, CodePatch
15
16
 
16
17
  # ── Lineage seal ──────────────────────────────────────────────────────────────
17
- SEAL = "🛡️ Protected by HealSpace"
18
+ SEAL = "🛡️ Protected by HealSpace"
19
+ SEAL_URL = "https://pypi.org/project/healspace/"
20
+ SEAL_MD = "🛡️ Protected by HealSpace — [click here to learn more](https://pypi.org/project/healspace/)"
18
21
 
19
22
  # ── Auto-fix actions ──────────────────────────────────────────────────────────
20
23
 
@@ -221,7 +224,7 @@ class HealSpace:
221
224
  for r in reports:
222
225
  lines.append(str(r))
223
226
  lines.append("")
224
- lines.append(SEAL)
227
+ lines.append(f"{SEAL} — learn more: {SEAL_URL}")
225
228
  return "\n".join(lines)
226
229
 
227
230
  def _trigger_error() -> str:
@@ -236,7 +239,71 @@ class HealSpace:
236
239
  heal_btn.click (_run_heal, inputs=log_input, outputs=output_box)
237
240
  error_btn.click(_trigger_error, inputs=None, outputs=output_box)
238
241
 
239
- gr.Markdown(f"<center><sub>{SEAL}</sub></center>")
242
+ # ── Mini AI Code Healer panel ──────────────────────────────────
243
+ with gr.Accordion("🧠 HealSpace AI — Code Healer", open=False):
244
+ gr.Markdown(
245
+ "Paste a **traceback** and the **broken source code** below.\n\n"
246
+ "The AI reads the style of your code (indentation, quotes, naming, "
247
+ "type hints) and writes a drop-in fix that matches it perfectly."
248
+ )
249
+ with gr.Row():
250
+ ai_tb_input = gr.Textbox(
251
+ label = "Traceback / Error",
252
+ placeholder = "Paste the full traceback here…",
253
+ lines = 6,
254
+ )
255
+ ai_code_input = gr.Textbox(
256
+ label = "Source Code (optional — paste the full file)",
257
+ placeholder = "Paste your app.py or the relevant module here…",
258
+ lines = 6,
259
+ )
260
+ ai_lineno = gr.Number(
261
+ label = "Error line number (from traceback)",
262
+ value = 1,
263
+ minimum = 1,
264
+ step = 1,
265
+ precision = 0,
266
+ )
267
+ ai_btn = gr.Button("🧠 Analyse & Generate Fix", variant="primary")
268
+ ai_output = gr.Code(
269
+ label = "HealSpace AI — Generated Fix",
270
+ language = "python",
271
+ lines = 14,
272
+ interactive = True,
273
+ )
274
+ ai_diff = gr.Textbox(
275
+ label = "Diff View",
276
+ lines = 10,
277
+ interactive = False,
278
+ )
279
+
280
+ def _run_ai_heal(tb_text: str, code_text: str, lineno: int) -> tuple:
281
+ healer = CodeHealer(verbose=False)
282
+ if code_text.strip():
283
+ patch = healer.heal_code(
284
+ code = code_text,
285
+ error_text = tb_text,
286
+ lineno = max(1, int(lineno)),
287
+ )
288
+ elif tb_text.strip():
289
+ patch = healer.heal_traceback(tb_text)
290
+ else:
291
+ return ("# Paste a traceback above first.", "")
292
+ if patch is None:
293
+ return (
294
+ "# HealSpace AI: no known error pattern matched.\n"
295
+ "# Try pasting more of the traceback.",
296
+ "",
297
+ )
298
+ return patch.fixed, patch.diff()
299
+
300
+ ai_btn.click(
301
+ _run_ai_heal,
302
+ inputs = [ai_tb_input, ai_code_input, ai_lineno],
303
+ outputs = [ai_output, ai_diff],
304
+ )
305
+
306
+ gr.Markdown(f"<center><sub>{SEAL_MD}</sub></center>")
240
307
 
241
308
  return self
242
309
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: healspace
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: HealSpace — Guardian AI for Hugging Face Spaces
5
5
  Author: Onyxl · TeraBites
6
6
  License: Apache-2.0
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  healspace/__init__.py
4
+ healspace/brain.py
4
5
  healspace/core.py
5
6
  healspace/errors.py
6
7
  healspace.egg-info/PKG-INFO
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "healspace"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "HealSpace — Guardian AI for Hugging Face Spaces"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1,34 +0,0 @@
1
- """
2
- HealSpace — Guardian AI for Hugging Face Spaces
3
- ================================================
4
- Automatically detects backend errors, heals common issues,
5
- and protects your Space with a branded lineage seal.
6
-
7
- Quick start
8
- -----------
9
- In your Space's app.py, wrap your Gradio app:
10
-
11
- import gradio as gr
12
- from healspace import heal
13
-
14
- with gr.Blocks() as demo:
15
- gr.Markdown("# My Space")
16
- # ... your UI ...
17
-
18
- heal(demo) # wraps, protects, and launches
19
- demo.launch()
20
-
21
- Or use the standalone guardian (non-Gradio):
22
-
23
- from healspace import HealSpace
24
- hs = HealSpace()
25
- hs.watch() # starts background health monitor
26
- """
27
-
28
- from .core import HealSpace, heal, diagnose, fix
29
- from .errors import KnownError, ErrorReport
30
-
31
- __version__ = "0.2.0"
32
- __author__ = "Onyxl · TeraBites"
33
- __license__ = "Apache 2.0"
34
- __all__ = ["HealSpace", "heal", "diagnose", "fix", "KnownError", "ErrorReport"]
File without changes
File without changes
File without changes