NeonCharlie-24 Daniel McKnight commited on
Commit
a93e37a
·
unverified ·
1 Parent(s): 484a699

Feat/auto persona colors (#23)

Browse files

* add auto-generated persona colors based on name hash.

* updated color picker to use the colorhash package.

* comment out current persona colors to test auto generation.

* removed unused import.

* Added auto_color and lucide_icons tests to CI pytest command.

* uncommented the manually selected colors for the 10 phd advisor personas.

* Apply suggestion from @NeonDaniel

---------

Co-authored-by: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com>

.github/workflows/unit_tests.yml CHANGED
@@ -27,7 +27,7 @@ jobs:
27
  - name: Run unit tests
28
  working-directory: multi_llm_chatbot_backend
29
  run: |
30
- pytest app/tests/test_persona_config.py --junitxml=test-results/results.xml
31
 
32
  - name: Upload test results
33
  if: always()
 
27
  - name: Run unit tests
28
  working-directory: multi_llm_chatbot_backend
29
  run: |
30
+ pytest app/tests/test_persona_config.py app/tests/test_auto_colors.py app/tests/test_lucide_icons.py --junitxml=test-results/results.xml
31
 
32
  - name: Upload test results
33
  if: always()
multi_llm_chatbot_backend/app/config.py CHANGED
@@ -9,8 +9,10 @@ working.
9
 
10
  import os
11
  import logging
 
12
  from pathlib import Path
13
  from typing import List, Optional
 
14
 
15
  import yaml
16
  from pydantic import BaseModel, validator, Field, model_validator
@@ -88,14 +90,24 @@ class PersonaItemConfig(_IconValidatorMixin):
88
  enabled: bool = True
89
  role: str = ""
90
  summary: str = ""
91
- color: str = "#6B7280"
92
- bg_color: str = "#F3F4F6"
93
- dark_color: str = "#9CA3AF"
94
- dark_bg_color: str = "#374151"
95
  icon: str = "HelpCircle"
96
  temperature: int = 5
97
  persona_prompt: str = ""
98
 
 
 
 
 
 
 
 
 
 
 
99
  def to_frontend_config(self) -> dict:
100
  return {
101
  "id": self.id,
@@ -293,7 +305,7 @@ def get_settings() -> AppSettings:
293
 
294
 
295
  # ---------------------------------------------------------------------------
296
- # Personas loader from directory
297
  # ---------------------------------------------------------------------------
298
 
299
 
@@ -346,4 +358,23 @@ def load_personas_from_dir(personas_dir: str) -> List[PersonaItemConfig]:
346
  personas.append(persona)
347
 
348
  logger.info(f"Loaded {len(personas)} persona(s) from {personas_dir}")
349
- return personas
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  import os
11
  import logging
12
+ import colorsys
13
  from pathlib import Path
14
  from typing import List, Optional
15
+ from colorhash import ColorHash
16
 
17
  import yaml
18
  from pydantic import BaseModel, validator, Field, model_validator
 
90
  enabled: bool = True
91
  role: str = ""
92
  summary: str = ""
93
+ color: Optional[str] = None
94
+ bg_color: Optional[str] = None
95
+ dark_color: Optional[str] = None
96
+ dark_bg_color: Optional[str] = None
97
  icon: str = "HelpCircle"
98
  temperature: int = 5
99
  persona_prompt: str = ""
100
 
101
+ @model_validator(mode='after')
102
+ def _auto_generate_colors(self):
103
+ if self.color is None:
104
+ generated = generate_persona_colors(self.name)
105
+ self.color = generated["color"]
106
+ self.bg_color = generated["bg_color"]
107
+ self.dark_color = generated["dark_color"]
108
+ self.dark_bg_color = generated["dark_bg_color"]
109
+ return self
110
+
111
  def to_frontend_config(self) -> dict:
112
  return {
113
  "id": self.id,
 
305
 
306
 
307
  # ---------------------------------------------------------------------------
308
+ # Helper Functions
309
  # ---------------------------------------------------------------------------
310
 
311
 
 
358
  personas.append(persona)
359
 
360
  logger.info(f"Loaded {len(personas)} persona(s) from {personas_dir}")
361
+ return personas
362
+
363
+
364
+ def generate_persona_colors(name: str) -> dict:
365
+ """Deterministically generate four theme colors from a persona name."""
366
+
367
+ ch = ColorHash(name.lower(), lightness=[0.55], saturation=[0.65])
368
+ hue = ch.hsl[0] # grab the hue colorhash picked
369
+ h = hue / 360
370
+
371
+ def hsl_to_hex(h, s, l):
372
+ r, g, b = colorsys.hls_to_rgb(h, l, s)
373
+ return f"#{int(r*255):02X}{int(g*255):02X}{int(b*255):02X}"
374
+
375
+ return {
376
+ "color": hsl_to_hex(h, 0.65, 0.55),
377
+ "bg_color": hsl_to_hex(h, 0.60, 0.95),
378
+ "dark_color": hsl_to_hex(h, 0.70, 0.70),
379
+ "dark_bg_color": hsl_to_hex(h, 0.65, 0.25),
380
+ }
multi_llm_chatbot_backend/app/tests/test_auto_colors.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import pytest
3
+ from app.config import PersonaItemConfig, generate_persona_colors
4
+
5
+
6
+ HEX_COLOR_RE = re.compile(r"^#[0-9A-Fa-f]{6}$")
7
+
8
+
9
+ def _perceived_lightness(hex_color: str) -> float:
10
+ """0.0 = black, 1.0 = white. Weighted formula matching human perception."""
11
+ h = hex_color.lstrip("#")
12
+ r, g, b = (int(h[i:i+2], 16) for i in (0, 2, 4))
13
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # generate_persona_colors tests
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ def test_all_values_are_valid_hex():
22
+ colors = generate_persona_colors("Methodologist")
23
+ for key in ("color", "bg_color", "dark_color", "dark_bg_color"):
24
+ assert key in colors, f"Missing key: {key}"
25
+ assert HEX_COLOR_RE.match(colors[key]), f"{key}={colors[key]} is not valid hex"
26
+
27
+
28
+ def test_deterministic():
29
+ a = generate_persona_colors("Methodologist")
30
+ b = generate_persona_colors("Methodologist")
31
+ assert a == b
32
+
33
+
34
+ def test_bg_color_is_light_and_dark_bg_is_dark():
35
+ colors = generate_persona_colors("Methodologist")
36
+ assert _perceived_lightness(colors["bg_color"]) > 0.85
37
+ assert _perceived_lightness(colors["dark_bg_color"]) < 0.35
38
+
39
+
40
+ def test_explicit_color_preserved():
41
+ p = PersonaItemConfig(id="test", name="Methodologist", color="#FF0000")
42
+ assert p.color == "#FF0000"
multi_llm_chatbot_backend/requirements.txt CHANGED
@@ -15,6 +15,9 @@ python-docx
15
  python-dotenv
16
  pyyaml~=6.0
17
 
 
 
 
18
  # Vector database and embeddings
19
  chromadb
20
  sentence-transformers
 
15
  python-dotenv
16
  pyyaml~=6.0
17
 
18
+ # Persona color generation
19
+ colorhash~=2.3
20
+
21
  # Vector database and embeddings
22
  chromadb
23
  sentence-transformers