NeonClary Cursor commited on
Commit
cc7440f
·
1 Parent(s): 294f234

Add Cybersecurity Advisor Canvas on FEAT_CybersecurityCanvas

Browse files

Rebrand Canvas-Upgrades into a cybersecurity panel with six advisor personas,
Neon BrainForge Security on 4090 x1-3 (CybersecurityExpert/vanilla pile personas),
GPT-5.4 fallback with 3s race-to-first-response, datetime orchestrator tool,
and updated UI copy, user guide, and canvas tour.

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (41) hide show
  1. cybersecurity_config.yaml +173 -0
  2. docker-compose.yml +9 -4
  3. multi_llm_chatbot_backend/.env.example +12 -0
  4. multi_llm_chatbot_backend/app/config.py +17 -0
  5. multi_llm_chatbot_backend/app/core/bootstrap.py +87 -16
  6. multi_llm_chatbot_backend/app/core/improved_orchestrator.py +7 -9
  7. multi_llm_chatbot_backend/app/llm/improved_vllm_client.py +49 -9
  8. multi_llm_chatbot_backend/app/llm/neon_pile.py +67 -0
  9. multi_llm_chatbot_backend/app/llm/openai_fallback_client.py +140 -0
  10. multi_llm_chatbot_backend/app/llm/resilient_client.py +130 -0
  11. multi_llm_chatbot_backend/app/models/default_personas.py +21 -2
  12. multi_llm_chatbot_backend/app/tests/unit/test_course_search_tool.py +0 -33
  13. multi_llm_chatbot_backend/app/tests/unit/test_current_datetime_tool.py +21 -0
  14. multi_llm_chatbot_backend/app/tests/unit/test_resilient_client.py +49 -0
  15. multi_llm_chatbot_backend/app/tests/unit/test_rmp_tool.py +0 -169
  16. multi_llm_chatbot_backend/app/tests/unit/test_tool_registry.py +3 -82
  17. multi_llm_chatbot_backend/app/tools/current_datetime.py +62 -0
  18. multi_llm_chatbot_backend/app/tools/rate_my_professor.py +0 -202
  19. multi_llm_chatbot_backend/app/tools/search_courses.py +0 -191
  20. multi_llm_chatbot_backend/requirements.txt +1 -0
  21. personas/cyber_advisors/compliance_officer.yaml +19 -0
  22. personas/cyber_advisors/incident_responder.yaml +19 -0
  23. personas/cyber_advisors/jerry_huaute.yaml +20 -0
  24. personas/cyber_advisors/security_architect.yaml +19 -0
  25. personas/cyber_advisors/security_mentor.yaml +19 -0
  26. personas/cyber_advisors/threat_modeler.yaml +19 -0
  27. personas/phd_advisors/critic.yaml +0 -49
  28. personas/phd_advisors/empathetic.yaml +0 -49
  29. personas/phd_advisors/methodologist.yaml +0 -42
  30. personas/phd_advisors/minimalist.yaml +0 -49
  31. personas/phd_advisors/motivator.yaml +0 -49
  32. personas/phd_advisors/pragmatist.yaml +0 -48
  33. personas/phd_advisors/socratic.yaml +0 -46
  34. personas/phd_advisors/storyteller.yaml +0 -48
  35. personas/phd_advisors/theorist.yaml +0 -45
  36. personas/phd_advisors/visionary.yaml +0 -48
  37. phd-advisor-frontend/package-lock.json +31 -0
  38. phd-advisor-frontend/src/components/CopyrightNotice.js +1 -1
  39. phd-advisor-frontend/src/components/canvas/CanvasWelcomeTour.js +8 -8
  40. phd-advisor-frontend/src/components/canvas/canvasData.js +18 -18
  41. phd-advisor-frontend/src/data/userGuide.js +37 -68
cybersecurity_config.yaml ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # Cybersecurity Advisor Canvas — Application Configuration
3
+ # ============================================================================
4
+
5
+ app:
6
+ title: "Cybersecurity Advisor Canvas"
7
+ subtitle: "AI-Powered Security Guidance"
8
+ primary_color: "#0F172A"
9
+ logo_icon: "Shield"
10
+ footer_text: "© 2026 Neon AI. All rights reserved."
11
+
12
+ homepage:
13
+ headline_prefix: "Strengthen Your Security Posture with"
14
+ headline_highlight: "Expert AI Advisors"
15
+ description: >-
16
+ Get practical guidance on threats, compliance, incident response, architecture,
17
+ and career growth from a panel of cybersecurity-focused AI advisors — each
18
+ bringing a distinct lens to your questions.
19
+ features_title: "Why Use Cybersecurity Advisor Canvas?"
20
+ features:
21
+ - title: "Defense in Depth"
22
+ description: "Receive layered perspectives on risk, controls, detection, and response"
23
+ icon: "Shield"
24
+ - title: "Neon Security Model"
25
+ description: "Powered by BrainForge Security on Neon 4090 x1-3 with GPT-5.4 fallback"
26
+ icon: "Brain"
27
+ - title: "Available 24/7"
28
+ description: "Get answers during incidents, audits, study sessions, or late-night architecture reviews"
29
+ icon: "Clock"
30
+
31
+ login:
32
+ subtitle: "Sign in to continue your security journey"
33
+ signup_subtitle: "Create your account for personalized guidance from our cybersecurity advisor panel"
34
+ academic_stages:
35
+ - { value: "", label: "Select your experience level" }
36
+ - { value: "student", label: "Student / Learner" }
37
+ - { value: "career-changer", label: "Career Changer" }
38
+ - { value: "junior-analyst", label: "Junior Analyst" }
39
+ - { value: "soc-analyst", label: "SOC Analyst" }
40
+ - { value: "engineer", label: "Security Engineer" }
41
+ - { value: "architect", label: "Architect / Lead" }
42
+ - { value: "manager", label: "Manager / Director" }
43
+
44
+ chat_page:
45
+ placeholder: "Ask your advisors about threats, controls, incidents, compliance, or your security career..."
46
+ examples:
47
+ - title: "Threats & Defense"
48
+ icon: "ShieldAlert"
49
+ color: "#DC2626"
50
+ bg_color: "#FEF2F2"
51
+ suggestions:
52
+ - "How do I prioritize vulnerabilities found in a recent scan?"
53
+ - "Walk me through STRIDE threat modeling for a new API"
54
+ - "What should our phishing simulation program measure?"
55
+ - title: "Compliance & Governance"
56
+ icon: "Scale"
57
+ color: "#2563EB"
58
+ bg_color: "#EFF6FF"
59
+ suggestions:
60
+ - "How do NIST CSF and ISO 27001 overlap for a mid-size company?"
61
+ - "What evidence do auditors expect for access reviews?"
62
+ - "How do I scope SOC 2 controls for a SaaS product?"
63
+ - title: "Incidents & IR"
64
+ icon: "Siren"
65
+ color: "#F59E0B"
66
+ bg_color: "#FFFBEB"
67
+ suggestions:
68
+ - "First 60 minutes after ransomware is detected — what do I do?"
69
+ - "How do I preserve forensic evidence on a compromised endpoint?"
70
+ - "When should we engage legal and PR during a breach?"
71
+ - title: "Career & Skills"
72
+ icon: "TrendingUp"
73
+ color: "#059669"
74
+ bg_color: "#ECFDF5"
75
+ suggestions:
76
+ - "CISSP vs Security+ — which path fits my background?"
77
+ - "How do I move from SOC tier 1 to detection engineering?"
78
+ - "What should I build in a home lab to stand out in interviews?"
79
+
80
+ personas:
81
+ base_prompt: |
82
+ **Formatting (Compact Markdown v1):**
83
+ - Use GitHub-Flavored Markdown.
84
+ - Output exactly three sections in this order:
85
+ - `### Thought` — one sentence.
86
+ - `### What to do` — exactly 3 bullets, one line each.
87
+ - `### Next step` — one imperative sentence.
88
+ - Use `###` for headings, `-` for bullets (no unicode bullets), keep number text on the same line (e.g., `1. Do X`).
89
+ - Insert one blank line between blocks.
90
+
91
+ personas_dir: "personas/cyber_advisors"
92
+
93
+ orchestrator:
94
+ min_words_without_keywords: 6
95
+ specific_keywords:
96
+ - "security"
97
+ - "cyber"
98
+ - "threat"
99
+ - "vulnerability"
100
+ - "CVE"
101
+ - "malware"
102
+ - "ransomware"
103
+ - "phishing"
104
+ - "IAM"
105
+ - "MFA"
106
+ - "SIEM"
107
+ - "SOC"
108
+ - "pentest"
109
+ - "compliance"
110
+ - "NIST"
111
+ - "ISO"
112
+ - "incident"
113
+ - "forensics"
114
+ - "firewall"
115
+ - "encryption"
116
+ - "zero trust"
117
+ - "cloud"
118
+ - "AWS"
119
+ - "Azure"
120
+ - "CISSP"
121
+ - "audit"
122
+
123
+ clarification_questions:
124
+ - "What specific security topic would you like guidance on?"
125
+ - "Are you focused on prevention, detection, response, compliance, or career growth?"
126
+ - "What is your role and the system or environment you're asking about?"
127
+ - "What's the most urgent risk or decision you're facing right now?"
128
+
129
+ clarification_suggestions:
130
+ - "Ask about threat modeling or hardening a workload"
131
+ - "Get help with incident response or forensics steps"
132
+ - "Request compliance or audit preparation guidance"
133
+ - "Upload a policy, architecture diagram, or log excerpt for review"
134
+
135
+ auth:
136
+ algorithm: "HS256"
137
+ token_expiry_minutes: 43200
138
+
139
+ mongodb:
140
+ database_name: "cybersecurity_advisor"
141
+
142
+ llm:
143
+ provider: "vllm"
144
+ gemini:
145
+ model: "gemini-2.5-flash"
146
+ ollama:
147
+ model: "llama3.2:1b"
148
+ vllm:
149
+ api_url: "https://4090-x1-3.neonaiservices2.com/vllm0"
150
+ api_key: ""
151
+ model_id: "BrainForge/Security@2026.03.18"
152
+ neon_persona_orchestrator: "vanilla"
153
+ neon_persona_advisors: "CybersecurityExpert"
154
+ openai:
155
+ api_key: ""
156
+ model: "gpt-5.4"
157
+ orchestrator_reasoning_effort: "low"
158
+ persona_reasoning_effort: "none"
159
+ resilient:
160
+ race_timeout_seconds: 3
161
+
162
+ rag:
163
+ embedding_model: "all-MiniLM-L6-v2"
164
+ chroma_collection: "cybersecurity_advisor_documents"
165
+
166
+ tools:
167
+ current_datetime:
168
+ enabled: true
169
+ default_timezone: "America/Los_Angeles"
170
+
171
+ voice:
172
+ stt_endpoint: "https://whisper.neonaiservices.com"
173
+ tts_endpoint: "https://coqui.neonaiservices.com"
docker-compose.yml CHANGED
@@ -12,26 +12,31 @@ services:
12
  - database
13
  environment:
14
  MONGODB_CONNECTION_STRING: "mongodb://database:27017"
15
- MONGODB_DATABASE: phd_advisor
16
  JWT_SECRET_KEY: ${JWT_SECRET_KEY:-CHANGEME-by-overriding-in-dot-env-file}
17
  GEMINI_API_KEY: ${GEMINI_API_KEY:-?}
18
  VLLM_API_KEY: ${VLLM_API_KEY:-}
19
- CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000}
20
  GEMINI_MODEL: gemini-2.5-flash
21
- CONFIG_PATH: ${CONFIG_PATH:-/ccai/phd_config.yaml}
 
22
  frontend:
23
  build:
24
  context: .
25
  dockerfile: Dockerfile
26
  target: frontend
27
  ports:
28
- - "3000:3000"
29
  networks:
30
  - application
31
  depends_on:
32
  - backend
33
  environment:
34
  REACT_APP_API_URL: ${REACT_APP_API_URL:-http://localhost:8000}
 
 
 
 
35
  database:
36
  image: mongo:8.0
37
  volumes:
 
12
  - database
13
  environment:
14
  MONGODB_CONNECTION_STRING: "mongodb://database:27017"
15
+ MONGODB_DATABASE: cybersecurity_advisor
16
  JWT_SECRET_KEY: ${JWT_SECRET_KEY:-CHANGEME-by-overriding-in-dot-env-file}
17
  GEMINI_API_KEY: ${GEMINI_API_KEY:-?}
18
  VLLM_API_KEY: ${VLLM_API_KEY:-}
19
+ CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3010,http://localhost:3000}
20
  GEMINI_MODEL: gemini-2.5-flash
21
+ CONFIG_PATH: ${CONFIG_PATH:-/ccai/cybersecurity_config.yaml}
22
+ OPENAI_API_KEY: ${OPENAI_API_KEY:-}
23
  frontend:
24
  build:
25
  context: .
26
  dockerfile: Dockerfile
27
  target: frontend
28
  ports:
29
+ - "3010:3010"
30
  networks:
31
  - application
32
  depends_on:
33
  - backend
34
  environment:
35
  REACT_APP_API_URL: ${REACT_APP_API_URL:-http://localhost:8000}
36
+ PORT: "3010"
37
+ HOST: "0.0.0.0"
38
+ WDS_SOCKET_PORT: "3010"
39
+ BROWSER: "none"
40
  database:
41
  image: mongo:8.0
42
  volumes:
multi_llm_chatbot_backend/.env.example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Path to app config (use cybersecurity_config.yaml for this branch)
2
+ CONFIG_PATH=../cybersecurity_config.yaml
3
+
4
+ MONGODB_CONNECTION_STRING=mongodb://localhost:27017
5
+ JWT_SECRET_KEY=change-me
6
+
7
+ # From C:\Users\dream\.secrets\shared.env or set here:
8
+ VLLM_API_KEY=
9
+ OPENAI_API_KEY=
10
+ GEMINI_API_KEY=
11
+
12
+ CORS_ORIGINS=http://localhost:3010,http://127.0.0.1:3010
multi_llm_chatbot_backend/app/config.py CHANGED
@@ -263,12 +263,29 @@ class OllamaConfig(BaseModel):
263
  class VllmConfig(BaseModel):
264
  api_url: str = ""
265
  api_key: str = Field(default=os.getenv("VLLM_API_KEY", ""))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
 
268
  class LLMConfig(BaseModel):
 
269
  gemini: GeminiConfig = GeminiConfig()
270
  ollama: OllamaConfig = OllamaConfig()
271
  vllm: VllmConfig = VllmConfig()
 
 
272
 
273
 
274
  class RAGConfig(BaseModel):
 
263
  class VllmConfig(BaseModel):
264
  api_url: str = ""
265
  api_key: str = Field(default=os.getenv("VLLM_API_KEY", ""))
266
+ model_id: str = ""
267
+ neon_persona_orchestrator: str = "vanilla"
268
+ neon_persona_advisors: str = "CybersecurityExpert"
269
+
270
+
271
+ class OpenAIConfig(BaseModel):
272
+ api_key: str = Field(default=os.getenv("OPENAI_API_KEY", ""))
273
+ model: str = "gpt-5.4"
274
+ orchestrator_reasoning_effort: str = "low"
275
+ persona_reasoning_effort: str = "none"
276
+
277
+
278
+ class ResilientConfig(BaseModel):
279
+ race_timeout_seconds: float = 3.0
280
 
281
 
282
  class LLMConfig(BaseModel):
283
+ provider: str = "gemini"
284
  gemini: GeminiConfig = GeminiConfig()
285
  ollama: OllamaConfig = OllamaConfig()
286
  vllm: VllmConfig = VllmConfig()
287
+ openai: OpenAIConfig = OpenAIConfig()
288
+ resilient: ResilientConfig = ResilientConfig()
289
 
290
 
291
  class RAGConfig(BaseModel):
multi_llm_chatbot_backend/app/core/bootstrap.py CHANGED
@@ -1,37 +1,108 @@
1
  # app/core/bootstrap.py
 
 
 
2
  from app.config import get_settings
3
  from app.llm.improved_gemini_client import ImprovedGeminiClient
4
  from app.llm.improved_ollama_client import ImprovedOllamaClient
5
  from app.llm.improved_vllm_client import ImprovedVllmClient
 
 
6
  from app.core.improved_orchestrator import ImprovedChatOrchestrator
7
  from app.models.default_personas import get_default_personas
8
 
9
  settings = get_settings()
10
 
11
- current_provider = "gemini"
12
  available_providers = ["ollama", "gemini", "vllm"]
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def create_llm_client(provider=None):
15
  if provider is None:
16
  provider = current_provider
17
  if provider == "gemini":
18
  return ImprovedGeminiClient(model_name=settings.llm.gemini.model)
19
- elif provider == "vllm":
20
- if not settings.llm.vllm.api_url:
21
- raise ValueError("No vLLM endpoint configured. Set llm.vllm.api_url in your config.")
22
- return ImprovedVllmClient(
23
- api_url=settings.llm.vllm.api_url,
24
- api_key=settings.llm.vllm.api_key,
25
- )
26
- else:
27
- return ImprovedOllamaClient(
28
- model_name=settings.llm.ollama.model,
29
- base_url=settings.llm.ollama.base_url,
30
- )
31
-
32
- llm = create_llm_client()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  chat_orchestrator = ImprovedChatOrchestrator(llm_client=llm)
34
 
35
- DEFAULT_PERSONAS = get_default_personas(llm)
 
36
  for persona in DEFAULT_PERSONAS:
37
  chat_orchestrator.register_persona(persona)
 
1
  # app/core/bootstrap.py
2
+ import os
3
+ from pathlib import Path
4
+
5
  from app.config import get_settings
6
  from app.llm.improved_gemini_client import ImprovedGeminiClient
7
  from app.llm.improved_ollama_client import ImprovedOllamaClient
8
  from app.llm.improved_vllm_client import ImprovedVllmClient
9
+ from app.llm.openai_fallback_client import OpenAIFallbackClient
10
+ from app.llm.resilient_client import ResilientLLMClient
11
  from app.core.improved_orchestrator import ImprovedChatOrchestrator
12
  from app.models.default_personas import get_default_personas
13
 
14
  settings = get_settings()
15
 
16
+ current_provider = settings.llm.provider or "vllm"
17
  available_providers = ["ollama", "gemini", "vllm"]
18
 
19
+
20
+ def _load_shared_env_vllm_key() -> str:
21
+ shared = Path.home() / ".secrets" / "shared.env"
22
+ if shared.exists():
23
+ for line in shared.read_text(encoding="utf-8").splitlines():
24
+ line = line.strip()
25
+ if line.startswith("VLLM_API_KEY="):
26
+ return line.split("=", 1)[1].strip()
27
+ return ""
28
+
29
+
30
+ def _vllm_api_key() -> str:
31
+ return settings.llm.vllm.api_key or os.getenv("VLLM_API_KEY", "") or _load_shared_env_vllm_key()
32
+
33
+
34
+ def _openai_api_key() -> str:
35
+ return settings.llm.openai.api_key or os.getenv("OPENAI_API_KEY", "")
36
+
37
+
38
+ def _build_neon_vllm(neon_persona: str | None) -> ImprovedVllmClient:
39
+ vllm = settings.llm.vllm
40
+ if not vllm.api_url:
41
+ raise ValueError("No vLLM endpoint configured. Set llm.vllm.api_url in your config.")
42
+ model_name = vllm.model_id or None
43
+ return ImprovedVllmClient(
44
+ api_url=vllm.api_url,
45
+ api_key=_vllm_api_key(),
46
+ model_name=model_name,
47
+ neon_persona=neon_persona,
48
+ )
49
+
50
+
51
+ def _build_openai(reasoning_effort: str) -> OpenAIFallbackClient:
52
+ return OpenAIFallbackClient(
53
+ api_key=_openai_api_key(),
54
+ model=settings.llm.openai.model,
55
+ reasoning_effort=reasoning_effort,
56
+ )
57
+
58
+
59
+ def _wrap_resilient(primary: ImprovedVllmClient, fallback: OpenAIFallbackClient, label: str):
60
+ return ResilientLLMClient(
61
+ primary=primary,
62
+ fallback=fallback,
63
+ race_timeout_seconds=settings.llm.resilient.race_timeout_seconds,
64
+ primary_label=label,
65
+ )
66
+
67
+
68
  def create_llm_client(provider=None):
69
  if provider is None:
70
  provider = current_provider
71
  if provider == "gemini":
72
  return ImprovedGeminiClient(model_name=settings.llm.gemini.model)
73
+ if provider == "vllm":
74
+ neon = settings.llm.vllm.neon_persona_orchestrator
75
+ primary = _build_neon_vllm(neon if neon != "vanilla" else None)
76
+ fallback = _build_openai(settings.llm.openai.orchestrator_reasoning_effort)
77
+ return _wrap_resilient(primary, fallback, "orchestrator")
78
+ return ImprovedOllamaClient(
79
+ model_name=settings.llm.ollama.model,
80
+ base_url=settings.llm.ollama.base_url,
81
+ )
82
+
83
+
84
+ def create_orchestrator_llm():
85
+ if current_provider != "vllm":
86
+ return create_llm_client()
87
+ neon = settings.llm.vllm.neon_persona_orchestrator
88
+ primary = _build_neon_vllm(neon if neon != "vanilla" else None)
89
+ fallback = _build_openai(settings.llm.openai.orchestrator_reasoning_effort)
90
+ return _wrap_resilient(primary, fallback, "orchestrator")
91
+
92
+
93
+ def create_persona_llm():
94
+ if current_provider != "vllm":
95
+ return create_llm_client()
96
+ neon = settings.llm.vllm.neon_persona_advisors
97
+ primary = _build_neon_vllm(neon)
98
+ fallback = _build_openai(settings.llm.openai.persona_reasoning_effort)
99
+ return _wrap_resilient(primary, fallback, "persona")
100
+
101
+
102
+ llm = create_orchestrator_llm()
103
  chat_orchestrator = ImprovedChatOrchestrator(llm_client=llm)
104
 
105
+ persona_llm = create_persona_llm() if current_provider == "vllm" else llm
106
+ DEFAULT_PERSONAS = get_default_personas(persona_llm)
107
  for persona in DEFAULT_PERSONAS:
108
  chat_orchestrator.register_persona(persona)
multi_llm_chatbot_backend/app/core/improved_orchestrator.py CHANGED
@@ -61,15 +61,13 @@ class ImprovedChatOrchestrator:
61
  return ToolCallResult(text="", used_tool=False)
62
 
63
  system_prompt = (
64
- "You are a helpful assistant with access to external tools. "
65
- "Use the available tools when the user's question can be answered "
66
- "by one of them. If no tool is relevant, respond with a brief "
67
- "text answer. "
68
- "If a tool response includes 'truncated': true, let the user know "
69
- "how many total results were found and suggest they narrow their "
70
- "search for more specific results. "
71
- "Format your responses using markdown. Use bullet points "
72
- "to present structured data like course listings or professor ratings."
73
  )
74
 
75
  return await self.llm_client.generate_with_tools(
 
61
  return ToolCallResult(text="", used_tool=False)
62
 
63
  system_prompt = (
64
+ "You are a helpful cybersecurity assistant with access to external tools. "
65
+ "Use the available tools when the user's question can be answered by one of them. "
66
+ "Call get_current_datetime when the user asks about today, deadlines, timelines, "
67
+ "schedules, incident timing, or when accurate date/time context would improve "
68
+ "your guidance then weave the result into your answer. "
69
+ "If no tool is relevant, respond with a brief text answer. "
70
+ "Format your responses using markdown."
 
 
71
  )
72
 
73
  return await self.llm_client.generate_with_tools(
multi_llm_chatbot_backend/app/llm/improved_vllm_client.py CHANGED
@@ -5,23 +5,61 @@ from typing import Any, Callable, Dict, List, Optional
5
  from openai import AsyncOpenAI, APIConnectionError, APIStatusError
6
 
7
  from app.llm.llm_client import LLMClient, ToolCallInfo, ToolCallResult
 
8
  from app.core.context_manager import get_context_manager
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
 
13
  class ImprovedVllmClient(LLMClient):
14
- def __init__(self, api_url: str, api_key: str, model_name: str = None):
 
 
 
 
 
 
 
15
  self.api_url = api_url
16
  self.api_key = api_key
17
  self.model_name = model_name
 
 
18
  self.client = AsyncOpenAI(
19
  base_url=f"{api_url}/v1",
20
- api_key=api_key,
21
  timeout=90.0,
22
  )
23
  self.context_manager = get_context_manager()
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  async def refresh_model(self):
26
  """Query the vLLM endpoint to discover the currently loaded model."""
27
  models = await self.client.models.list()
@@ -35,8 +73,8 @@ class ImprovedVllmClient(LLMClient):
35
  try:
36
  context_window = self.context_manager.prepare_context_for_llm(
37
  messages=context,
38
- system_prompt=system_prompt,
39
- llm_provider="vllm"
40
  )
41
 
42
  logger.debug(f"Context prepared: {len(context_window.messages)} messages, "
@@ -45,9 +83,11 @@ class ImprovedVllmClient(LLMClient):
45
  if not self.model_name:
46
  await self.refresh_model()
47
 
 
 
48
  create_kwargs = dict(
49
  model=self.model_name,
50
- messages=context_window.messages,
51
  temperature=temperature,
52
  max_tokens=max_tokens,
53
  )
@@ -104,10 +144,10 @@ class ImprovedVllmClient(LLMClient):
104
  if not self.model_name:
105
  await self.refresh_model()
106
 
107
- messages: List[Dict[str, Any]] = [
108
- {"role": "system", "content": system_prompt},
109
- {"role": "user", "content": user_message},
110
- ]
111
 
112
  openai_tools = tool_definitions or []
113
 
 
5
  from openai import AsyncOpenAI, APIConnectionError, APIStatusError
6
 
7
  from app.llm.llm_client import LLMClient, ToolCallInfo, ToolCallResult
8
+ from app.llm.neon_pile import get_pile_system_prompt
9
  from app.core.context_manager import get_context_manager
10
 
11
  logger = logging.getLogger(__name__)
12
 
13
 
14
  class ImprovedVllmClient(LLMClient):
15
+ def __init__(
16
+ self,
17
+ api_url: str,
18
+ api_key: str,
19
+ model_name: str = None,
20
+ neon_persona: str | None = None,
21
+ model_revision: str | None = None,
22
+ ):
23
  self.api_url = api_url
24
  self.api_key = api_key
25
  self.model_name = model_name
26
+ self.neon_persona = neon_persona
27
+ self.model_revision = model_revision
28
  self.client = AsyncOpenAI(
29
  base_url=f"{api_url}/v1",
30
+ api_key=api_key or "not-needed",
31
  timeout=90.0,
32
  )
33
  self.context_manager = get_context_manager()
34
 
35
+ def _resolve_model_revision(self) -> str | None:
36
+ if not self.model_name or "@" not in self.model_name:
37
+ return self.model_revision
38
+ _, _, suffix = self.model_name.partition("@")
39
+ return suffix or self.model_revision
40
+
41
+ def _resolve_base_model_name(self) -> str:
42
+ if not self.model_name:
43
+ return ""
44
+ return self.model_name.split("@", 1)[0]
45
+
46
+ def _build_messages(self, system_prompt: str, context_messages: List[dict]) -> List[dict]:
47
+ """Prepend Neon pile persona system prompt when configured."""
48
+ messages: List[dict] = []
49
+ base_model = self._resolve_base_model_name()
50
+ if base_model and self.neon_persona:
51
+ pile_prompt = get_pile_system_prompt(
52
+ base_model,
53
+ self.neon_persona,
54
+ revision=self._resolve_model_revision(),
55
+ )
56
+ if pile_prompt:
57
+ messages.append({"role": "system", "content": pile_prompt})
58
+ if system_prompt:
59
+ messages.append({"role": "system", "content": system_prompt})
60
+ messages.extend(context_messages)
61
+ return messages
62
+
63
  async def refresh_model(self):
64
  """Query the vLLM endpoint to discover the currently loaded model."""
65
  models = await self.client.models.list()
 
73
  try:
74
  context_window = self.context_manager.prepare_context_for_llm(
75
  messages=context,
76
+ system_prompt="",
77
+ llm_provider="vllm",
78
  )
79
 
80
  logger.debug(f"Context prepared: {len(context_window.messages)} messages, "
 
83
  if not self.model_name:
84
  await self.refresh_model()
85
 
86
+ api_messages = self._build_messages(system_prompt, context_window.messages)
87
+
88
  create_kwargs = dict(
89
  model=self.model_name,
90
+ messages=api_messages,
91
  temperature=temperature,
92
  max_tokens=max_tokens,
93
  )
 
144
  if not self.model_name:
145
  await self.refresh_model()
146
 
147
+ messages: List[Dict[str, Any]] = self._build_messages(
148
+ system_prompt,
149
+ [{"role": "user", "content": user_message}],
150
+ )
151
 
152
  openai_tools = tool_definitions or []
153
 
multi_llm_chatbot_backend/app/llm/neon_pile.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load BrainForge pile personas (persona2system) from a HuggingFace model repo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from functools import lru_cache
7
+ from typing import Dict, Optional
8
+
9
+ import yaml
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _PILE_CACHE: Dict[str, Dict[str, Optional[str]]] = {}
14
+
15
+
16
+ def load_pile_personas(model_name: str, revision: Optional[str] = None) -> Dict[str, Optional[str]]:
17
+ """Return ``{persona_id: system_prompt_or_none}`` for a BrainForge model.
18
+
19
+ ``vanilla`` is always present with value ``None`` (no pile system prompt).
20
+ """
21
+ cache_key = f"{model_name}@{revision or 'default'}"
22
+ if cache_key in _PILE_CACHE:
23
+ return _PILE_CACHE[cache_key]
24
+
25
+ personas: Dict[str, Optional[str]] = {"vanilla": None}
26
+ try:
27
+ from huggingface_hub import hf_hub_download
28
+ from huggingface_hub.utils import EntryNotFoundError
29
+
30
+ config_path = hf_hub_download(
31
+ model_name,
32
+ "config.yaml",
33
+ subfolder="datasets",
34
+ revision=revision,
35
+ )
36
+ with open(config_path, "r", encoding="utf-8") as fh:
37
+ data = yaml.safe_load(fh) or {}
38
+ pile = data.get("pile") or {}
39
+ persona2system = pile.get("persona2system") or {}
40
+ for key, prompt in persona2system.items():
41
+ personas[str(key)] = prompt
42
+ personas.setdefault("vanilla", None)
43
+ logger.info(
44
+ "Loaded %d pile persona(s) for %s (keys: %s)",
45
+ len(personas),
46
+ model_name,
47
+ ", ".join(sorted(personas.keys())),
48
+ )
49
+ except EntryNotFoundError:
50
+ logger.warning("No datasets/config.yaml pile for model %s", model_name)
51
+ except Exception as exc:
52
+ logger.warning("Failed to load pile personas for %s: %s", model_name, exc)
53
+
54
+ _PILE_CACHE[cache_key] = personas
55
+ return personas
56
+
57
+
58
+ def get_pile_system_prompt(
59
+ model_name: str,
60
+ neon_persona: Optional[str],
61
+ revision: Optional[str] = None,
62
+ ) -> Optional[str]:
63
+ """Resolve a pile system prompt for *neon_persona*, or None for vanilla/missing."""
64
+ if not neon_persona or neon_persona == "vanilla":
65
+ return None
66
+ pile = load_pile_personas(model_name, revision=revision)
67
+ return pile.get(neon_persona)
multi_llm_chatbot_backend/app/llm/openai_fallback_client.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenAI GPT fallback client with configurable reasoning effort."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any, Callable, Dict, List, Optional
8
+
9
+ from openai import AsyncOpenAI, APIConnectionError, APIStatusError
10
+
11
+ from app.llm.llm_client import LLMClient, ToolCallInfo, ToolCallResult
12
+ from app.core.context_manager import get_context_manager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _VLLM_ERROR_MARKERS = (
17
+ "unable to connect",
18
+ "encountered an error",
19
+ "unexpected error",
20
+ )
21
+
22
+
23
+ class OpenAIFallbackClient(LLMClient):
24
+ def __init__(
25
+ self,
26
+ api_key: str,
27
+ model: str = "gpt-5.4",
28
+ reasoning_effort: Optional[str] = None,
29
+ ):
30
+ if not api_key:
31
+ raise ValueError("OpenAI API key not set. Provide OPENAI_API_KEY or llm.openai.api_key.")
32
+ self.model = model
33
+ self.reasoning_effort = reasoning_effort
34
+ self.client = AsyncOpenAI(api_key=api_key, timeout=120.0)
35
+ self.context_manager = get_context_manager()
36
+
37
+ def _reasoning_kwargs(self) -> Dict[str, Any]:
38
+ if not self.reasoning_effort or self.reasoning_effort == "none":
39
+ return {}
40
+ return {"reasoning_effort": self.reasoning_effort}
41
+
42
+ async def generate(
43
+ self,
44
+ system_prompt: str,
45
+ context: List[dict],
46
+ temperature: float,
47
+ max_tokens: int,
48
+ response_mime_type: str = None,
49
+ ) -> str:
50
+ context_window = self.context_manager.prepare_context_for_llm(
51
+ messages=context,
52
+ system_prompt=system_prompt,
53
+ llm_provider="openai",
54
+ )
55
+ create_kwargs: Dict[str, Any] = dict(
56
+ model=self.model,
57
+ messages=context_window.messages,
58
+ temperature=temperature,
59
+ max_tokens=max_tokens,
60
+ **self._reasoning_kwargs(),
61
+ )
62
+ if response_mime_type == "application/json":
63
+ create_kwargs["response_format"] = {"type": "json_object"}
64
+
65
+ try:
66
+ response = await self.client.chat.completions.create(**create_kwargs)
67
+ text = (response.choices[0].message.content or "").strip()
68
+ if not text:
69
+ raise ValueError("OpenAI returned empty content")
70
+ return self._clean_response(text)
71
+ except (APIConnectionError, APIStatusError) as exc:
72
+ logger.error("OpenAI API error: %s", exc)
73
+ raise
74
+ except Exception as exc:
75
+ logger.error("OpenAI generate failed: %s", exc)
76
+ raise
77
+
78
+ _MAX_TOOL_ROUNDS = 5
79
+
80
+ async def generate_with_tools(
81
+ self,
82
+ system_prompt: str,
83
+ user_message: str,
84
+ tool_definitions: Optional[List[Dict[str, Any]]] = None,
85
+ tool_executor: Optional[Callable] = None,
86
+ temperature: float = 0.7,
87
+ max_tokens: int = 2048,
88
+ ) -> ToolCallResult:
89
+ messages: List[Dict[str, Any]] = [
90
+ {"role": "system", "content": system_prompt},
91
+ {"role": "user", "content": user_message},
92
+ ]
93
+ openai_tools = tool_definitions or []
94
+ all_tool_calls: List[ToolCallInfo] = []
95
+
96
+ try:
97
+ for _round in range(self._MAX_TOOL_ROUNDS):
98
+ response = await self.client.chat.completions.create(
99
+ model=self.model,
100
+ messages=messages,
101
+ tools=openai_tools or None,
102
+ temperature=temperature,
103
+ max_tokens=max_tokens,
104
+ **self._reasoning_kwargs(),
105
+ )
106
+ choice = response.choices[0].message
107
+ if not choice.tool_calls:
108
+ text = choice.content or ""
109
+ if not text.strip():
110
+ raise ValueError("OpenAI tool loop returned empty content")
111
+ return ToolCallResult(
112
+ text=text,
113
+ used_tool=bool(all_tool_calls),
114
+ tool_name=all_tool_calls[0].name if all_tool_calls else None,
115
+ tool_args=all_tool_calls[0].args if all_tool_calls else {},
116
+ tool_calls_made=all_tool_calls,
117
+ )
118
+
119
+ messages.append(choice.model_dump())
120
+ for tc in choice.tool_calls:
121
+ fn_name = tc.function.name
122
+ fn_args = json.loads(tc.function.arguments)
123
+ all_tool_calls.append(ToolCallInfo(name=fn_name, args=fn_args))
124
+ try:
125
+ tool_result = await tool_executor(name=fn_name, **fn_args)
126
+ except Exception as exc:
127
+ tool_result = {"error": str(exc)}
128
+ messages.append({
129
+ "role": "tool",
130
+ "tool_call_id": tc.id,
131
+ "content": json.dumps(tool_result),
132
+ })
133
+
134
+ raise ValueError("OpenAI tool-calling loop exhausted max rounds")
135
+ except (APIConnectionError, APIStatusError) as exc:
136
+ logger.error("OpenAI tool API error: %s", exc)
137
+ raise
138
+ except Exception as exc:
139
+ logger.error("OpenAI generate_with_tools failed: %s", exc)
140
+ raise
multi_llm_chatbot_backend/app/llm/resilient_client.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Primary/fallback LLM wrapper with failure failover and optional race-to-first."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Any, Callable, Dict, List, Optional
8
+
9
+ from app.llm.llm_client import LLMClient, ToolCallResult
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ _VLLM_ERROR_MARKERS = (
14
+ "unable to connect",
15
+ "encountered an error",
16
+ "unexpected error",
17
+ )
18
+
19
+
20
+ def _looks_like_failed_response(text: str) -> bool:
21
+ if not text or not text.strip():
22
+ return True
23
+ lower = text.strip().lower()
24
+ return any(marker in lower for marker in _VLLM_ERROR_MARKERS)
25
+
26
+
27
+ class ResilientLLMClient(LLMClient):
28
+ """Try *primary* first; on failure or slow response, use *fallback*."""
29
+
30
+ def __init__(
31
+ self,
32
+ primary: LLMClient,
33
+ fallback: LLMClient,
34
+ race_timeout_seconds: float = 3.0,
35
+ primary_label: str = "primary",
36
+ ):
37
+ self.primary = primary
38
+ self.fallback = fallback
39
+ self.race_timeout_seconds = race_timeout_seconds
40
+ self.primary_label = primary_label
41
+
42
+ async def _run_primary(self, coro_factory):
43
+ result = await coro_factory(self.primary)
44
+ if isinstance(result, str) and _looks_like_failed_response(result):
45
+ raise RuntimeError(f"{self.primary_label} returned failure text")
46
+ if isinstance(result, ToolCallResult) and _looks_like_failed_response(result.text):
47
+ raise RuntimeError(f"{self.primary_label} tool call returned failure text")
48
+ return result
49
+
50
+ async def _run_fallback(self, coro_factory):
51
+ logger.info("Using fallback LLM for %s", self.primary_label)
52
+ return await coro_factory(self.fallback)
53
+
54
+ async def _race_or_fallback(self, coro_factory):
55
+ primary_task = asyncio.create_task(self._run_primary(coro_factory))
56
+ try:
57
+ return await asyncio.wait_for(
58
+ asyncio.shield(primary_task),
59
+ timeout=self.race_timeout_seconds,
60
+ )
61
+ except asyncio.TimeoutError:
62
+ logger.info(
63
+ "%s exceeded %.1fs — racing fallback",
64
+ self.primary_label,
65
+ self.race_timeout_seconds,
66
+ )
67
+ except Exception as exc:
68
+ logger.warning("%s failed: %s — using fallback", self.primary_label, exc)
69
+ if not primary_task.done():
70
+ primary_task.cancel()
71
+ return await self._run_fallback(coro_factory)
72
+
73
+ if primary_task.done():
74
+ try:
75
+ return primary_task.result()
76
+ except Exception as exc:
77
+ logger.warning("%s failed after wait: %s", self.primary_label, exc)
78
+ return await self._run_fallback(coro_factory)
79
+
80
+ fallback_task = asyncio.create_task(self._run_fallback(coro_factory))
81
+ done, pending = await asyncio.wait(
82
+ {primary_task, fallback_task},
83
+ return_when=asyncio.FIRST_COMPLETED,
84
+ )
85
+ for task in pending:
86
+ task.cancel()
87
+ for task in done:
88
+ if task.cancelled():
89
+ continue
90
+ try:
91
+ return task.result()
92
+ except Exception as exc:
93
+ logger.warning("Race winner failed: %s", exc)
94
+ return await self._run_fallback(coro_factory)
95
+
96
+ async def generate(
97
+ self,
98
+ system_prompt: str,
99
+ context: List[dict],
100
+ temperature: float,
101
+ max_tokens: int,
102
+ response_mime_type: str = None,
103
+ ) -> str:
104
+ async def _call(client: LLMClient):
105
+ return await client.generate(
106
+ system_prompt, context, temperature, max_tokens, response_mime_type,
107
+ )
108
+
109
+ return await self._race_or_fallback(_call)
110
+
111
+ async def generate_with_tools(
112
+ self,
113
+ system_prompt: str,
114
+ user_message: str,
115
+ tool_definitions: Optional[List[Dict[str, Any]]] = None,
116
+ tool_executor: Optional[Callable] = None,
117
+ temperature: float = 0.7,
118
+ max_tokens: int = 2048,
119
+ ) -> ToolCallResult:
120
+ async def _call(client: LLMClient):
121
+ return await client.generate_with_tools(
122
+ system_prompt=system_prompt,
123
+ user_message=user_message,
124
+ tool_definitions=tool_definitions,
125
+ tool_executor=tool_executor,
126
+ temperature=temperature,
127
+ max_tokens=max_tokens,
128
+ )
129
+
130
+ return await self._race_or_fallback(_call)
multi_llm_chatbot_backend/app/models/default_personas.py CHANGED
@@ -6,9 +6,10 @@ The heavy persona definitions have moved into ``config.yaml`` (under the
6
  and exposes the same public API the rest of the codebase already relies on.
7
  """
8
 
9
- from typing import List, Optional
10
 
11
  from app.config import get_settings
 
12
  from app.models.persona import Persona
13
 
14
 
@@ -46,7 +47,7 @@ def _get_registry() -> dict:
46
  # Public API — unchanged signatures so existing callers keep working
47
  # ------------------------------------------------------------------
48
 
49
- def get_default_personas(llm) -> List[Persona]:
50
  """Return a list of :class:`Persona` objects wired to *llm*."""
51
  return [
52
  Persona(
@@ -60,6 +61,24 @@ def get_default_personas(llm) -> List[Persona]:
60
  ]
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def get_default_persona_prompt(persona_id: str) -> Optional[str]:
64
  data = _get_registry().get(persona_id)
65
  return data["system_prompt"] if data else None
 
6
  and exposes the same public API the rest of the codebase already relies on.
7
  """
8
 
9
+ from typing import Dict, List, Optional
10
 
11
  from app.config import get_settings
12
+ from app.llm.llm_client import LLMClient
13
  from app.models.persona import Persona
14
 
15
 
 
47
  # Public API — unchanged signatures so existing callers keep working
48
  # ------------------------------------------------------------------
49
 
50
+ def get_default_personas(llm: LLMClient) -> List[Persona]:
51
  """Return a list of :class:`Persona` objects wired to *llm*."""
52
  return [
53
  Persona(
 
61
  ]
62
 
63
 
64
+ def get_personas_with_llm_map(
65
+ default_llm: LLMClient,
66
+ llm_map: Optional[Dict[str, LLMClient]] = None,
67
+ ) -> List[Persona]:
68
+ if not llm_map:
69
+ return get_default_personas(default_llm)
70
+ return [
71
+ Persona(
72
+ id=pid,
73
+ name=data["name"],
74
+ system_prompt=data["system_prompt"],
75
+ llm=llm_map.get(pid, default_llm),
76
+ temperature=data.get("default_temperature", 5),
77
+ )
78
+ for pid, data in _get_registry().items()
79
+ ]
80
+
81
+
82
  def get_default_persona_prompt(persona_id: str) -> Optional[str]:
83
  data = _get_registry().get(persona_id)
84
  return data["system_prompt"] if data else None
multi_llm_chatbot_backend/app/tests/unit/test_course_search_tool.py DELETED
@@ -1,33 +0,0 @@
1
- import asyncio
2
- import unittest
3
-
4
- from app.tools.search_courses import TOOL_DEFINITION, execute
5
-
6
-
7
- class TestSearchCoursesContract(unittest.TestCase):
8
- """The search_courses tool module must export a valid OpenAI
9
- tool definition and an async executor."""
10
-
11
- def test_tool_definition_has_required_fields(self):
12
- self.assertEqual(TOOL_DEFINITION["type"], "function")
13
- self.assertIn("function", TOOL_DEFINITION)
14
- fn = TOOL_DEFINITION["function"]
15
- self.assertIn("name", fn)
16
- self.assertIn("description", fn)
17
- self.assertIn("parameters", fn)
18
-
19
- def test_tool_definition_name(self):
20
- self.assertEqual(TOOL_DEFINITION["function"]["name"], "search_courses")
21
-
22
- def test_tool_definition_has_nonempty_description(self):
23
- self.assertIsInstance(TOOL_DEFINITION["function"]["description"], str)
24
- self.assertGreater(len(TOOL_DEFINITION["function"]["description"]), 0)
25
-
26
- def test_tool_definition_parameters_is_valid_schema(self):
27
- params = TOOL_DEFINITION["function"]["parameters"]
28
- self.assertEqual(params["type"], "object")
29
- self.assertIn("properties", params)
30
- self.assertIn("subject", params["properties"])
31
-
32
- def test_execute_is_async_callable(self):
33
- self.assertTrue(asyncio.iscoroutinefunction(execute))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tests/unit/test_current_datetime_tool.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import unittest
3
+ from unittest.mock import patch
4
+
5
+ from app.tools.current_datetime import TOOL_DEFINITION, execute
6
+
7
+
8
+ class TestCurrentDatetimeTool(unittest.TestCase):
9
+ def test_tool_definition_shape(self):
10
+ self.assertEqual(TOOL_DEFINITION["type"], "function")
11
+ self.assertEqual(TOOL_DEFINITION["function"]["name"], "get_current_datetime")
12
+
13
+ @patch("app.config.get_settings")
14
+ def test_execute_returns_utc_and_local(self, mock_settings):
15
+ mock_settings.return_value.tools.get_tool_config.return_value = {
16
+ "default_timezone": "UTC",
17
+ }
18
+ result = asyncio.run(execute())
19
+ self.assertIn("utc_iso", result)
20
+ self.assertIn("local_iso", result)
21
+ self.assertEqual(result["local_timezone"], "UTC")
multi_llm_chatbot_backend/app/tests/unit/test_resilient_client.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import unittest
3
+ from unittest.mock import AsyncMock, MagicMock
4
+
5
+ from app.llm.llm_client import ToolCallResult
6
+ from app.llm.resilient_client import ResilientLLMClient, _looks_like_failed_response
7
+
8
+
9
+ class TestResilientHelpers(unittest.TestCase):
10
+ def test_failed_response_detection(self):
11
+ self.assertTrue(_looks_like_failed_response(""))
12
+ self.assertTrue(_looks_like_failed_response("I'm unable to connect to the AI service."))
13
+ self.assertFalse(_looks_like_failed_response("Hello world"))
14
+
15
+
16
+ class TestResilientClient(unittest.TestCase):
17
+ def test_primary_failure_uses_fallback(self):
18
+ primary = MagicMock()
19
+ primary.generate = AsyncMock(
20
+ return_value="I'm unable to connect to the AI service. Please ensure the vLLM endpoint is available.",
21
+ )
22
+ fallback = MagicMock()
23
+ fallback.generate = AsyncMock(return_value="fallback answer")
24
+
25
+ client = ResilientLLMClient(primary, fallback, race_timeout_seconds=3.0)
26
+ result = asyncio.run(
27
+ client.generate("sys", [{"role": "user", "content": "hi"}], 0.5, 100),
28
+ )
29
+ self.assertEqual(result, "fallback answer")
30
+ fallback.generate.assert_awaited_once()
31
+
32
+ def test_race_uses_faster_fallback(self):
33
+ async def slow_primary(*_a, **_k):
34
+ await asyncio.sleep(5)
35
+ return "primary"
36
+
37
+ async def fast_fallback(*_a, **_k):
38
+ return "fallback fast"
39
+
40
+ primary = MagicMock()
41
+ primary.generate = slow_primary
42
+ fallback = MagicMock()
43
+ fallback.generate = fast_fallback
44
+
45
+ client = ResilientLLMClient(primary, fallback, race_timeout_seconds=0.05)
46
+ result = asyncio.run(
47
+ client.generate("sys", [{"role": "user", "content": "hi"}], 0.5, 100),
48
+ )
49
+ self.assertEqual(result, "fallback fast")
multi_llm_chatbot_backend/app/tests/unit/test_rmp_tool.py DELETED
@@ -1,169 +0,0 @@
1
- import asyncio
2
- import unittest
3
- from unittest.mock import AsyncMock, MagicMock, patch
4
-
5
- from app.tools.rate_my_professor import TOOL_DEFINITION, execute
6
-
7
-
8
- def _graphql_success_response(nodes):
9
- """Build a mock RMP GraphQL response containing the given teacher nodes."""
10
- edges = [{"cursor": f"c{i}", "node": n} for i, n in enumerate(nodes)]
11
- return {
12
- "data": {
13
- "search": {
14
- "teachers": {
15
- "didFallback": False,
16
- "edges": edges,
17
- "pageInfo": {"hasNextPage": False, "endCursor": ""},
18
- }
19
- }
20
- }
21
- }
22
-
23
-
24
- SAMPLE_NODE = {
25
- "id": "VGVhY2hlci0xMjM0",
26
- "legacyId": 1234,
27
- "firstName": "Jane",
28
- "lastName": "Smith",
29
- "department": "Computer Science",
30
- "school": {"id": "U2Nob29sLTEwODc=", "name": "University of Colorado Boulder"},
31
- "avgRating": 4.2,
32
- "avgDifficulty": 3.1,
33
- "wouldTakeAgainPercent": 85.0,
34
- "numRatings": 42,
35
- }
36
-
37
-
38
- class TestRMPToolContract(unittest.TestCase):
39
- """The rate_my_professor tool module must export a valid OpenAI
40
- tool definition and an async executor."""
41
-
42
- def test_tool_definition_has_required_fields(self):
43
- self.assertEqual(TOOL_DEFINITION["type"], "function")
44
- self.assertIn("function", TOOL_DEFINITION)
45
- fn = TOOL_DEFINITION["function"]
46
- self.assertIn("name", fn)
47
- self.assertIn("description", fn)
48
- self.assertIn("parameters", fn)
49
-
50
- def test_tool_definition_name(self):
51
- self.assertEqual(TOOL_DEFINITION["function"]["name"], "rate_my_professor")
52
-
53
- def test_tool_definition_has_nonempty_description(self):
54
- self.assertIsInstance(TOOL_DEFINITION["function"]["description"], str)
55
- self.assertGreater(len(TOOL_DEFINITION["function"]["description"]), 0)
56
-
57
- def test_tool_definition_parameters_schema(self):
58
- params = TOOL_DEFINITION["function"]["parameters"]
59
- self.assertEqual(params["type"], "object")
60
- self.assertIn("properties", params)
61
- self.assertIn("professor_name", params["properties"])
62
-
63
- def test_execute_is_async_callable(self):
64
- self.assertTrue(asyncio.iscoroutinefunction(execute))
65
-
66
-
67
- def _fake_tool_config(name):
68
- """Return a fake tool config dict with school_id set."""
69
- if name == "rate_my_professor":
70
- return {"enabled": True, "school_id": "U2Nob29sLTEwODc="}
71
- return {}
72
-
73
-
74
- @patch("app.tools.rate_my_professor.get_settings")
75
- class TestRMPToolExecutor(unittest.TestCase):
76
- """Unit tests for rate_my_professor.execute() with mocked HTTP."""
77
-
78
- def _mock_client(self, get_response, post_response):
79
- """Build a mock httpx.AsyncClient with canned GET and POST responses."""
80
- get_resp = MagicMock()
81
- get_resp.text = '<script>"Authorization":"Basic dGVzdDp0ZXN0"</script>'
82
- get_resp.raise_for_status = MagicMock()
83
- if get_response is not None:
84
- get_resp.text = get_response
85
-
86
- post_resp = MagicMock()
87
- post_resp.status_code = 200
88
- post_resp.json.return_value = post_response
89
- post_resp.raise_for_status = MagicMock()
90
-
91
- client_instance = AsyncMock()
92
- client_instance.get = AsyncMock(return_value=get_resp)
93
- client_instance.post = AsyncMock(return_value=post_resp)
94
-
95
- ctx = MagicMock()
96
- ctx.__aenter__ = AsyncMock(return_value=client_instance)
97
- ctx.__aexit__ = AsyncMock(return_value=False)
98
- return ctx, client_instance
99
-
100
- def test_execute_returns_professor_data(self, mock_get_settings):
101
- """Successful GraphQL response returns structured professor data."""
102
- mock_get_settings.return_value.tools.get_tool_config = _fake_tool_config
103
- ctx, client = self._mock_client(
104
- get_response=None,
105
- post_response=_graphql_success_response([SAMPLE_NODE]),
106
- )
107
-
108
- with patch("httpx.AsyncClient", return_value=ctx):
109
- result = asyncio.run(execute(professor_name="Smith"))
110
-
111
- self.assertIn("professors", result)
112
- self.assertEqual(len(result["professors"]), 1)
113
-
114
- prof = result["professors"][0]
115
- self.assertEqual(prof["name"], "Jane Smith")
116
- self.assertEqual(prof["department"], "Computer Science")
117
- self.assertAlmostEqual(prof["rating"], 4.2)
118
- self.assertAlmostEqual(prof["difficulty"], 3.1)
119
- self.assertEqual(prof["num_ratings"], 42)
120
-
121
- def test_execute_returns_empty_on_no_results(self, mock_get_settings):
122
- """When the GraphQL API returns no matching professors, return
123
- an empty list — not an error."""
124
- mock_get_settings.return_value.tools.get_tool_config = _fake_tool_config
125
- ctx, _ = self._mock_client(
126
- get_response=None,
127
- post_response=_graphql_success_response([]),
128
- )
129
-
130
- with patch("httpx.AsyncClient", return_value=ctx):
131
- result = asyncio.run(execute(professor_name="Nonexistent"))
132
-
133
- self.assertIn("professors", result)
134
- self.assertEqual(len(result["professors"]), 0)
135
-
136
- def test_execute_returns_error_on_api_failure(self, mock_get_settings):
137
- """When the HTTP request fails, return an error payload instead
138
- of raising an exception."""
139
- mock_get_settings.return_value.tools.get_tool_config = _fake_tool_config
140
- ctx = MagicMock()
141
- client_instance = AsyncMock()
142
- client_instance.get = AsyncMock(side_effect=Exception("connection refused"))
143
- client_instance.post = AsyncMock(side_effect=Exception("connection refused"))
144
- ctx.__aenter__ = AsyncMock(return_value=client_instance)
145
- ctx.__aexit__ = AsyncMock(return_value=False)
146
-
147
- with patch("httpx.AsyncClient", return_value=ctx):
148
- result = asyncio.run(execute(professor_name="Smith"))
149
-
150
- self.assertIn("professors", result)
151
- self.assertEqual(len(result["professors"]), 0)
152
- self.assertIn("error", result)
153
-
154
- def test_execute_accepts_name_kwarg(self, mock_get_settings):
155
- """The dispatcher passes name= as a kwarg; execute must accept
156
- and ignore it without error."""
157
- mock_get_settings.return_value.tools.get_tool_config = _fake_tool_config
158
- ctx, _ = self._mock_client(
159
- get_response=None,
160
- post_response=_graphql_success_response([SAMPLE_NODE]),
161
- )
162
-
163
- with patch("httpx.AsyncClient", return_value=ctx):
164
- result = asyncio.run(
165
- execute(name="rate_my_professor", professor_name="Smith")
166
- )
167
-
168
- self.assertIn("professors", result)
169
- self.assertEqual(len(result["professors"]), 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tests/unit/test_tool_registry.py CHANGED
@@ -10,13 +10,10 @@ from app.tools import (
10
  )
11
 
12
 
13
- KNOWN_TOOLS = {"search_courses", "rate_my_professor"}
14
 
15
 
16
  class TestToolDiscovery(unittest.TestCase):
17
- """Auto-discovery should find every tool module that exports
18
- TOOL_DEFINITION + execute."""
19
-
20
  def test_known_tools_are_discovered(self):
21
  registered = set(list_registered_tools())
22
  for name in KNOWN_TOOLS:
@@ -27,97 +24,21 @@ class TestToolDiscovery(unittest.TestCase):
27
  self.assertIn("definition", entry, f"'{name}' missing definition")
28
  self.assertIn("executor", entry, f"'{name}' missing executor")
29
 
30
- def test_definitions_have_required_fields(self):
31
- for name, entry in _REGISTRY.items():
32
- defn = entry["definition"]
33
- self.assertEqual(defn["type"], "function")
34
- self.assertIn("function", defn)
35
- fn = defn["function"]
36
- self.assertIn("name", fn)
37
- self.assertIn("description", fn)
38
- self.assertIn("parameters", fn)
39
- self.assertEqual(fn["name"], name)
40
-
41
- def test_executors_are_async_callables(self):
42
- for name, entry in _REGISTRY.items():
43
- self.assertTrue(
44
- asyncio.iscoroutinefunction(entry["executor"]),
45
- f"Executor for '{name}' is not an async function",
46
- )
47
-
48
 
49
  class TestGetToolDefinitions(unittest.TestCase):
50
- """get_tool_definitions() returns OpenAI-format tool dicts,
51
- optionally filtered."""
52
-
53
  def test_returns_all_when_no_filter(self):
54
  defs = get_tool_definitions()
55
  names = {d["function"]["name"] for d in defs}
56
  self.assertTrue(KNOWN_TOOLS.issubset(names))
57
 
58
  def test_filter_to_single_tool(self):
59
- defs = get_tool_definitions(enabled=["search_courses"])
60
  self.assertEqual(len(defs), 1)
61
- self.assertEqual(defs[0]["function"]["name"], "search_courses")
62
-
63
- def test_filter_to_multiple_tools(self):
64
- defs = get_tool_definitions(enabled=["search_courses", "rate_my_professor"])
65
- names = {d["function"]["name"] for d in defs}
66
- self.assertEqual(names, KNOWN_TOOLS)
67
-
68
- def test_filter_with_unknown_name_returns_empty(self):
69
- defs = get_tool_definitions(enabled=["nonexistent_tool"])
70
- self.assertEqual(defs, [])
71
-
72
- def test_filter_with_empty_list_returns_empty(self):
73
- defs = get_tool_definitions(enabled=[])
74
- self.assertEqual(defs, [])
75
-
76
- def test_filter_ignores_unknown_names_keeps_valid(self):
77
- defs = get_tool_definitions(enabled=["search_courses", "bogus"])
78
- self.assertEqual(len(defs), 1)
79
- self.assertEqual(defs[0]["function"]["name"], "search_courses")
80
 
81
 
82
  class TestGetToolExecutor(unittest.TestCase):
83
- """get_tool_executor() returns a dispatcher that routes to the
84
- correct tool executor."""
85
-
86
- def test_dispatch_known_tool(self):
87
- mock_exec = AsyncMock(return_value={"courses": []})
88
- original = _REGISTRY["search_courses"]["executor"]
89
- _REGISTRY["search_courses"]["executor"] = mock_exec
90
- try:
91
- dispatch = get_tool_executor()
92
- result = asyncio.run(dispatch(name="search_courses", subject="CSCI"))
93
- mock_exec.assert_called_once_with(name="search_courses", subject="CSCI")
94
- self.assertEqual(result, {"courses": []})
95
- finally:
96
- _REGISTRY["search_courses"]["executor"] = original
97
-
98
  def test_dispatch_unknown_tool_returns_error(self):
99
  dispatch = get_tool_executor()
100
  result = asyncio.run(dispatch(name="nonexistent"))
101
  self.assertIn("error", result)
102
-
103
- def test_filtered_executor_allows_enabled_tool(self):
104
- mock_exec = AsyncMock(return_value={"courses": []})
105
- original = _REGISTRY["search_courses"]["executor"]
106
- _REGISTRY["search_courses"]["executor"] = mock_exec
107
- try:
108
- dispatch = get_tool_executor(enabled=["search_courses"])
109
- result = asyncio.run(dispatch(name="search_courses", subject="CSCI"))
110
- self.assertNotIn("error", result)
111
- finally:
112
- _REGISTRY["search_courses"]["executor"] = original
113
-
114
- def test_filtered_executor_blocks_disabled_tool(self):
115
- dispatch = get_tool_executor(enabled=["search_courses"])
116
- result = asyncio.run(dispatch(name="rate_my_professor", professor_name="Smith"))
117
- self.assertIn("error", result)
118
- self.assertIn("not enabled", result["error"])
119
-
120
- def test_filtered_executor_with_empty_list_blocks_all(self):
121
- dispatch = get_tool_executor(enabled=[])
122
- result = asyncio.run(dispatch(name="search_courses", subject="CSCI"))
123
- self.assertIn("error", result)
 
10
  )
11
 
12
 
13
+ KNOWN_TOOLS = {"get_current_datetime"}
14
 
15
 
16
  class TestToolDiscovery(unittest.TestCase):
 
 
 
17
  def test_known_tools_are_discovered(self):
18
  registered = set(list_registered_tools())
19
  for name in KNOWN_TOOLS:
 
24
  self.assertIn("definition", entry, f"'{name}' missing definition")
25
  self.assertIn("executor", entry, f"'{name}' missing executor")
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  class TestGetToolDefinitions(unittest.TestCase):
 
 
 
29
  def test_returns_all_when_no_filter(self):
30
  defs = get_tool_definitions()
31
  names = {d["function"]["name"] for d in defs}
32
  self.assertTrue(KNOWN_TOOLS.issubset(names))
33
 
34
  def test_filter_to_single_tool(self):
35
+ defs = get_tool_definitions(enabled=["get_current_datetime"])
36
  self.assertEqual(len(defs), 1)
37
+ self.assertEqual(defs[0]["function"]["name"], "get_current_datetime")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
 
40
  class TestGetToolExecutor(unittest.TestCase):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def test_dispatch_unknown_tool_returns_error(self):
42
  dispatch = get_tool_executor()
43
  result = asyncio.run(dispatch(name="nonexistent"))
44
  self.assertIn("error", result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tools/current_datetime.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Return the current date and time for orchestrator context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone as dt_timezone
6
+ from typing import Any, Dict
7
+
8
+ try:
9
+ from zoneinfo import ZoneInfo
10
+ except ImportError: # pragma: no cover
11
+ ZoneInfo = None # type: ignore
12
+
13
+ TOOL_DEFINITION = {
14
+ "type": "function",
15
+ "function": {
16
+ "name": "get_current_datetime",
17
+ "description": (
18
+ "Get the current date and time in UTC and in a configured local timezone. "
19
+ "Use when the user asks about today, deadlines, timelines, schedules, "
20
+ "or when accurate temporal context improves security guidance."
21
+ ),
22
+ "parameters": {
23
+ "type": "object",
24
+ "properties": {
25
+ "timezone": {
26
+ "type": "string",
27
+ "description": (
28
+ "IANA timezone name (e.g. America/Los_Angeles). "
29
+ "Optional; defaults to the app-configured timezone."
30
+ ),
31
+ },
32
+ },
33
+ "required": [],
34
+ },
35
+ },
36
+ }
37
+
38
+
39
+ async def execute(name: str = "", timezone: str | None = None, **_: Any) -> Dict[str, Any]:
40
+ from app.config import get_settings
41
+
42
+ cfg = get_settings().tools.get_tool_config("current_datetime")
43
+ tz_name = timezone or cfg.get("default_timezone") or "UTC"
44
+ tz = dt_timezone.utc
45
+ if ZoneInfo is not None:
46
+ try:
47
+ tz = ZoneInfo(tz_name)
48
+ except Exception:
49
+ tz_name = "UTC"
50
+ tz = dt_timezone.utc
51
+
52
+ now_utc = datetime.now(dt_timezone.utc)
53
+ now_local = now_utc.astimezone(tz)
54
+
55
+ return {
56
+ "utc_iso": now_utc.isoformat(),
57
+ "local_iso": now_local.isoformat(),
58
+ "local_timezone": tz_name,
59
+ "local_weekday": now_local.strftime("%A"),
60
+ "local_date": now_local.strftime("%Y-%m-%d"),
61
+ "local_time": now_local.strftime("%H:%M:%S"),
62
+ }
multi_llm_chatbot_backend/app/tools/rate_my_professor.py DELETED
@@ -1,202 +0,0 @@
1
- """
2
- rate_my_professor tool — live query against RateMyProfessors' GraphQL API.
3
-
4
- Exposes TOOL_DEFINITION (OpenAI tool format) and an execute() coroutine
5
- that the tool-calling loop dispatches to.
6
-
7
- Requires ``school_id`` in the tool config (see phd_config.yaml).
8
- Use ``scripts/rmp_school_lookup.py`` to find the ID for a given school.
9
- """
10
-
11
- import logging
12
- import re
13
- from typing import Any, Dict, List
14
- import httpx
15
- from app.tools import BROWSER_UA
16
- from app.config import get_settings
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
- RMP_GRAPHQL_URL = "https://www.ratemyprofessors.com/graphql"
21
- RMP_LANDING_URL = "https://www.ratemyprofessors.com/"
22
- RMP_SEARCH_URL = "https://www.ratemyprofessors.com/search/professors/1087"
23
-
24
-
25
- TEACHER_SEARCH_QUERY = """
26
- query TeacherSearchPaginationQuery(
27
- $count: Int!
28
- $cursor: String
29
- $query: TeacherSearchQuery!
30
- ) {
31
- search: newSearch {
32
- teachers(query: $query, first: $count, after: $cursor) {
33
- didFallback
34
- edges {
35
- cursor
36
- node {
37
- id
38
- legacyId
39
- firstName
40
- lastName
41
- department
42
- school { id name }
43
- avgRating
44
- avgDifficulty
45
- wouldTakeAgainPercent
46
- numRatings
47
- }
48
- }
49
- pageInfo {
50
- hasNextPage
51
- endCursor
52
- }
53
- }
54
- }
55
- }
56
- """
57
-
58
- TOOL_DEFINITION: Dict[str, Any] = {
59
- "type": "function",
60
- "function": {
61
- "name": "rate_my_professor",
62
- "description": (
63
- "Look up RateMyProfessors ratings for a CU Boulder professor. "
64
- "Returns rating, difficulty, percentage of students who would "
65
- "take the professor again, and number of ratings."
66
- ),
67
- "parameters": {
68
- "type": "object",
69
- "properties": {
70
- "professor_name": {
71
- "type": "string",
72
- "description": (
73
- "Full or partial name of the professor to search for, "
74
- "e.g. 'Hoenigman', 'Jane Smith'."
75
- ),
76
- },
77
- },
78
- "required": ["professor_name"],
79
- },
80
- },
81
- }
82
-
83
-
84
- def _node_to_professor(node: Dict[str, Any]) -> Dict[str, Any]:
85
- """Convert a GraphQL teacher node to a lightweight result dict."""
86
- return {
87
- "name": f"{node.get('firstName', '')} {node.get('lastName', '')}".strip(),
88
- "department": node.get("department", ""),
89
- "rating": node.get("avgRating", 0),
90
- "difficulty": node.get("avgDifficulty", 0),
91
- "would_take_again_pct": node.get("wouldTakeAgainPercent", -1),
92
- "num_ratings": node.get("numRatings", 0),
93
- "rmp_id": node.get("id", ""),
94
- }
95
-
96
-
97
- async def _extract_auth_token(client: httpx.AsyncClient) -> str:
98
- """Fetch the RMP landing page and extract the auth token from the JS bundle.
99
-
100
- Falls back to the well-known Basic test:test token.
101
- """
102
- try:
103
- resp = await client.get(
104
- RMP_LANDING_URL, headers={"User-Agent": BROWSER_UA},
105
- )
106
- m = re.search(
107
- r'"Authorization"\s*:\s*"(Basic\s+[A-Za-z0-9+/=]+)"', resp.text,
108
- )
109
- if m:
110
- logger.info("Extracted RMP auth token from page JS")
111
- return m.group(1)
112
- except Exception as exc:
113
- logger.debug("RMP auth token extraction failed: %s", exc)
114
-
115
- return "Basic dGVzdDp0ZXN0"
116
-
117
-
118
- async def execute(
119
- *,
120
- name: str = "",
121
- professor_name: str,
122
- ) -> Dict[str, Any]:
123
- """Query RateMyProfessors for a CU Boulder professor by name.
124
-
125
- The 'name' kwarg is passed by the dispatch loop and ignored here.
126
- Returns {"professors": [...], "query": {...}}.
127
- """
128
- tool_cfg = get_settings().tools.get_tool_config("rate_my_professor")
129
- school_id = tool_cfg.get("school_id")
130
- if not school_id:
131
- logger.error("No school_id configured for rate_my_professor")
132
- return {
133
- "professors": [],
134
- "error": "No school_id configured for rate_my_professor",
135
- "query": {"professor_name": professor_name},
136
- }
137
-
138
- professors: List[Dict[str, Any]] = []
139
-
140
- try:
141
- async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
142
- auth_token = await _extract_auth_token(client)
143
-
144
- headers = {
145
- "User-Agent": BROWSER_UA,
146
- "Authorization": auth_token,
147
- "Content-Type": "application/json",
148
- "Referer": f"{RMP_SEARCH_URL}?q={professor_name}",
149
- "Origin": "https://www.ratemyprofessors.com",
150
- }
151
-
152
- variables = {
153
- "count": 20,
154
- "cursor": "",
155
- "query": {
156
- "text": professor_name,
157
- "schoolID": school_id,
158
- "fallback": True,
159
- "departmentID": None,
160
- },
161
- }
162
-
163
- resp = await client.post(
164
- RMP_GRAPHQL_URL,
165
- json={"query": TEACHER_SEARCH_QUERY, "variables": variables},
166
- headers=headers,
167
- )
168
-
169
- if resp.status_code == 403:
170
- logger.warning("RMP GraphQL returned 403 — auth may be invalid")
171
- return {
172
- "professors": [],
173
- "error": "RateMyProfessors authentication failed",
174
- "query": {"professor_name": professor_name},
175
- }
176
-
177
- resp.raise_for_status()
178
- data = resp.json()
179
-
180
- teachers = (
181
- data.get("data", {})
182
- .get("search", {})
183
- .get("teachers", {})
184
- )
185
-
186
- for edge in teachers.get("edges", []):
187
- node = edge.get("node", {})
188
- if node:
189
- professors.append(_node_to_professor(node))
190
-
191
- except Exception as exc:
192
- logger.error("RMP API error for %s: %s", professor_name, exc)
193
- return {
194
- "professors": [],
195
- "error": str(exc),
196
- "query": {"professor_name": professor_name},
197
- }
198
-
199
- return {
200
- "professors": professors,
201
- "query": {"professor_name": professor_name},
202
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tools/search_courses.py DELETED
@@ -1,191 +0,0 @@
1
- """
2
- search_courses tool — live query against CU Boulder's FOSE class-search API.
3
-
4
- Exposes TOOL_DEFINITION (OpenAI tool format) and an execute() coroutine
5
- that the tool-calling loop dispatches to.
6
- """
7
-
8
- import logging
9
- import re
10
- from typing import Any, Dict, List, Optional
11
- import httpx
12
- from app.tools import BROWSER_UA
13
- from app.config import get_settings
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- FOSE_SEARCH_URL = "https://classes.colorado.edu/api/?page=fose&route=search"
18
- CLASSES_BASE_URL = "https://classes.colorado.edu"
19
-
20
- TOOL_DEFINITION: Dict[str, Any] = {
21
- "type": "function",
22
- "function": {
23
- "name": "search_courses",
24
- "description": (
25
- "Search the CU Boulder course catalog for classes in a given "
26
- "subject, optionally filtered by course number and semester. "
27
- "Returns a list of matching sections with title, instructor, "
28
- "schedule, and location."
29
- ),
30
- "parameters": {
31
- "type": "object",
32
- "properties": {
33
- "subject": {
34
- "type": "string",
35
- "description": (
36
- "Department / subject code, e.g. 'CSCI', 'MATH', 'PHYS'."
37
- ),
38
- },
39
- "course_number": {
40
- "type": "string",
41
- "description": (
42
- "Catalog number to filter on, e.g. '1300'. "
43
- "Omit to return all courses in the subject."
44
- ),
45
- },
46
- "semester": {
47
- "type": "string",
48
- "description": (
49
- "Semester name, e.g. 'Spring 2026', 'Fall 2025'. "
50
- "Defaults to 'Spring 2026' if not provided."
51
- ),
52
- },
53
- },
54
- "required": ["subject"],
55
- },
56
- },
57
- }
58
-
59
- def _term_to_srcdb(term: str) -> str:
60
- """Convert 'Spring 2026' to '2261', 'Fall 2025' to '2257', etc.
61
-
62
- CU Boulder's FOSE API uses a 4-digit code: literal '2', the last
63
- two digits of the year, and a season digit (1=Spring, 4=Summer, 7=Fall).
64
- """
65
- term_lower = term.lower()
66
- ym = re.search(r"20(\d{2})", term)
67
- yy = ym.group(1) if ym else "26"
68
- if "spring" in term_lower:
69
- return f"2{yy}1"
70
- if "summer" in term_lower:
71
- return f"2{yy}4"
72
- if "fall" in term_lower:
73
- return f"2{yy}7"
74
- return f"2{yy}1"
75
-
76
-
77
- def _parse_schedule(meets: str) -> Dict[str, str]:
78
- """Parse 'MWF 10:00am-10:50am' into structured fields."""
79
- if not meets:
80
- return {"days": "", "start_time": "", "end_time": "", "raw": ""}
81
-
82
- day_match = re.match(r"([A-Za-z]+)", meets)
83
- days = day_match.group(1) if day_match else ""
84
-
85
- time_match = re.search(
86
- r"(\d{1,2}:\d{2}\s*[ap]m)\s*-\s*(\d{1,2}:\d{2}\s*[ap]m)", meets, re.I
87
- )
88
- start = time_match.group(1).strip() if time_match else ""
89
- end = time_match.group(2).strip() if time_match else ""
90
-
91
- return {"days": days, "start_time": start, "end_time": end, "raw": meets}
92
-
93
-
94
- def _row_to_course(item: Dict[str, Any], term: str) -> Optional[Dict[str, Any]]:
95
- """Convert a FOSE result row to a lightweight course dict.
96
- Returns None for rows that should be skipped (recitations, cancelled sections, etc.).
97
- """
98
- schd = item.get("schd", "")
99
- if schd and schd not in ("LEC", "SEM", ""):
100
- return None
101
- if item.get("isCancelled"):
102
- return None
103
-
104
- code = item.get("code", "").strip()
105
- if not code:
106
- code = (
107
- f"{item.get('subject', '')} "
108
- f"{item.get('catalog_nbr', item.get('catalogNbr', ''))}"
109
- ).strip()
110
-
111
- return {
112
- "course_code": code,
113
- "title": item.get("title", ""),
114
- "section": item.get("no", "") or item.get("section", ""),
115
- "instructor": item.get("instr", "") or item.get("instructor", "Staff"),
116
- "schedule": _parse_schedule(item.get("meets", "") or ""),
117
- "location": item.get("bldg", item.get("location", "")),
118
- "semester": term,
119
- }
120
-
121
-
122
-
123
- async def execute(
124
- *,
125
- name: str = "",
126
- subject: str,
127
- course_number: str = "",
128
- semester: str = "Spring 2026",
129
- ) -> Dict[str, Any]:
130
- """Query the CU Boulder FOSE API and return matching courses.
131
-
132
- The 'name' kwarg is passed by the dispatch loop and ignored here.
133
- Returns {"courses": [...], "query": {...}}.
134
- """
135
- srcdb = _term_to_srcdb(semester)
136
- subject = subject.upper().strip()
137
-
138
- payload = {
139
- "other": {"srcdb": srcdb},
140
- "criteria": [{"field": "subject", "value": subject}],
141
- }
142
- headers = {
143
- "User-Agent": BROWSER_UA,
144
- "Content-Type": "application/json",
145
- "Referer": CLASSES_BASE_URL,
146
- "Origin": CLASSES_BASE_URL,
147
- }
148
-
149
- courses: List[Dict[str, Any]] = []
150
-
151
- try:
152
- async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
153
- resp = await client.post(
154
- FOSE_SEARCH_URL, json=payload, headers=headers,
155
- )
156
- if resp.status_code != 200:
157
- logger.warning("FOSE API returned %s for %s", resp.status_code, subject)
158
- return {"courses": [], "query": {"subject": subject, "semester": semester}}
159
-
160
- body = resp.json()
161
- results = body.get("results", body.get("data", []))
162
-
163
- for item in results:
164
- row = _row_to_course(item, semester)
165
- if row:
166
- courses.append(row)
167
-
168
- except Exception as exc:
169
- logger.error("FOSE API error for %s: %s", subject, exc)
170
- return {"courses": [], "error": str(exc), "query": {"subject": subject, "semester": semester}}
171
-
172
- if course_number:
173
- cn = course_number.strip()
174
- courses = [c for c in courses if cn in c["course_code"]]
175
-
176
- max_results = get_settings().tools.get_tool_config("search_courses").get("max_results", 20)
177
-
178
- total = len(courses)
179
- truncated = total > max_results
180
- courses = courses[:max_results]
181
-
182
- return {
183
- "courses": courses,
184
- "total_results": total,
185
- "truncated": truncated,
186
- "query": {
187
- "subject": subject,
188
- "course_number": course_number or None,
189
- "semester": semester,
190
- },
191
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/requirements.txt CHANGED
@@ -6,6 +6,7 @@ python-multipart~=0.0
6
  # HTTP client for LLM APIs
7
  httpx~=0.28
8
  openai~=2.30
 
9
 
10
  # Document processing
11
  PyPDF2~=3.0
 
6
  # HTTP client for LLM APIs
7
  httpx~=0.28
8
  openai~=2.30
9
+ huggingface-hub~=0.36
10
 
11
  # Document processing
12
  PyPDF2~=3.0
personas/cyber_advisors/compliance_officer.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: compliance_officer
2
+ name: "Compliance & Governance Advisor"
3
+ role: "Frameworks & Audit Readiness"
4
+ summary: "Policy-minded & risk-based"
5
+ icon: "Scale"
6
+ temperature: 3
7
+ persona_prompt: |
8
+ You are a compliance and governance advisor experienced with NIST CSF, NIST 800-53, ISO 27001, SOC 2, PCI DSS, and HIPAA-style programs.
9
+
10
+ **YOUR EXPERTISE:**
11
+ - Control selection, evidence collection, and audit preparation
12
+ - Risk registers, exception handling, and third-party risk
13
+ - Translating regulations into practical control objectives
14
+ - Balancing security requirements with business operations
15
+
16
+ **YOUR RESPONSE STYLE:**
17
+ - Cite frameworks by name; distinguish mandatory vs recommended
18
+ - Provide checklist-style next steps for audits and assessments
19
+ - Flag when legal counsel or a QSA is required
personas/cyber_advisors/incident_responder.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: incident_responder
2
+ name: "Incident Response Lead"
3
+ role: "IR Playbooks & Forensics"
4
+ summary: "Calm under pressure"
5
+ icon: "Siren"
6
+ temperature: 4
7
+ persona_prompt: |
8
+ You are an incident response lead with experience in ransomware, BEC, credential theft, and cloud compromise scenarios across SOCs and CSIRTs.
9
+
10
+ **YOUR EXPERTISE:**
11
+ - First-hour containment, evidence preservation, and communication trees
12
+ - Log analysis, timeline reconstruction, and scope determination
13
+ - Coordination with legal, PR, and executive stakeholders
14
+ - Post-incident reviews and control improvements
15
+
16
+ **YOUR RESPONSE STYLE:**
17
+ - Prioritize life-safety and business continuity, then evidence integrity
18
+ - Use numbered steps for active incidents; distinguish urgent vs follow-up
19
+ - Be explicit about what not to do (e.g., wiping systems before imaging)
personas/cyber_advisors/jerry_huaute.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: jerry_huaute
2
+ name: "Jerry Huaute Advisor"
3
+ role: "Enterprise Security Mentor"
4
+ summary: "Direct, warm, experience-led"
5
+ color: "#0F766E"
6
+ bg_color: "#CCFBF1"
7
+ dark_color: "#5EEAD4"
8
+ dark_bg_color: "#134E4A"
9
+ icon: "User"
10
+ temperature: 4
11
+ persona_prompt: |
12
+ You are Tawish Jerry Huaute, CISSP, a Chumash man who spent 14+ years at Microsoft as a Senior Field Technical Account Manager starting in 1994. You grew up in Pomona, California. Your grandfather Semu Huaute (1908–2004) was a legendary Chumash medicine man, activist, and founder of the Red Wind Foundation who participated in the 1969 Alcatraz occupation. You are a member of Native Americans at Microsoft, mentoring Indigenous youth in tech careers.
13
+
14
+ Your core beliefs: "Don't let an opportunity pass you by. You need to invest in yourself." Keep learning, seize chances, and build your value through education and hands-on experience. You are proud of your Chumash heritage and see it as inseparable from your professional identity — not a contradiction.
15
+
16
+ Your tone is direct, warm, unpretentious, and encouraging. You speak from lived experience, not theory. You naturally mentor younger people, especially Native youth who may not see representation in tech. You know enterprise IT deeply — deployment, troubleshooting, account management, customer relationships, Microsoft technologies. You don't lead with credentials; you lead with real talk.
17
+
18
+ When discussing heritage, speak with quiet pride about your family and Chumash culture. When giving career advice, be practical and honest — you believe in people but expect them to put in the work.
19
+
20
+ Apply your security lens to mentorship: certifications, interviews, stakeholder communication, and building trust with IT and business leaders.
personas/cyber_advisors/security_architect.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: security_architect
2
+ name: "Security Architect"
3
+ role: "Zero Trust & Cloud Hardening"
4
+ summary: "Design-first & pragmatic"
5
+ icon: "Network"
6
+ temperature: 4
7
+ persona_prompt: |
8
+ You are a security architect specializing in zero trust, identity-centric access, network segmentation, and secure cloud landing zones (AWS, Azure, GCP).
9
+
10
+ **YOUR EXPERTISE:**
11
+ - Reference architectures, defense in depth, and secure defaults
12
+ - IAM, secrets management, encryption, and key lifecycle
13
+ - DevSecOps integration: CI/CD gates, SBOM, container security
14
+ - Trade-offs between usability, cost, and security posture
15
+
16
+ **YOUR RESPONSE STYLE:**
17
+ - Diagram concepts in prose when helpful (tiers, flows, trust zones)
18
+ - Recommend phased rollouts with quick wins and long-term targets
19
+ - Call out common misconfigurations and compensating controls
personas/cyber_advisors/security_mentor.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: security_mentor
2
+ name: "Security Career Mentor"
3
+ role: "Certs, Interviews & Growth"
4
+ summary: "Encouraging & practical"
5
+ icon: "TrendingUp"
6
+ temperature: 5
7
+ persona_prompt: |
8
+ You are a cybersecurity career mentor who has helped professionals move from help desk to SOC, from analyst to engineer, and into GRC and leadership tracks.
9
+
10
+ **YOUR EXPERTISE:**
11
+ - Certification paths (Security+, CySA+, CISSP, cloud vendor certs)
12
+ - Portfolio projects, home labs, and resume storytelling
13
+ - Interview preparation for SOC, IR, AppSec, and GRC roles
14
+ - Negotiation, burnout prevention, and continuous learning habits
15
+
16
+ **YOUR RESPONSE STYLE:**
17
+ - Be encouraging but honest about effort and timelines
18
+ - Suggest concrete weekly actions and measurable milestones
19
+ - Tailor advice to the user's stated experience level
personas/cyber_advisors/threat_modeler.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id: threat_modeler
2
+ name: "Threat Modeling Analyst"
3
+ role: "Attack Surface & STRIDE Specialist"
4
+ summary: "Structured & adversary-focused"
5
+ icon: "Crosshair"
6
+ temperature: 4
7
+ persona_prompt: |
8
+ You are a senior threat modeling analyst with deep experience in STRIDE, PASTA, attack trees, and MITRE ATT&CK mapping for cloud and on-prem systems.
9
+
10
+ **YOUR EXPERTISE:**
11
+ - Threat modeling workshops and data-flow diagrams
12
+ - Abuse cases, trust boundaries, and control gaps
13
+ - Prioritizing risks by likelihood, impact, and exploitability
14
+ - Mapping detections and mitigations to ATT&CK techniques
15
+
16
+ **YOUR RESPONSE STYLE:**
17
+ - Be precise and structured; name assumptions explicitly
18
+ - Tie recommendations to concrete controls (prevent, detect, respond)
19
+ - Ask clarifying questions about architecture and assets when needed
personas/phd_advisors/critic.yaml DELETED
@@ -1,49 +0,0 @@
1
- id: critic
2
- name: "Constructive Critic"
3
- role: "Academic Quality Analyst"
4
- summary: "Detail-oriented & Standards-focused"
5
- color: "#DC2626"
6
- bg_color: "#FEF2F2"
7
- dark_color: "#F87171"
8
- dark_bg_color: "#7F1D1D"
9
- icon: "Search"
10
- avatar: "advisor1.png"
11
- temperature: 6
12
- persona_prompt: |
13
- You are a rigorous PhD advisor and Constructive Critic with expertise in academic quality assurance and scholarly rigor. With a PhD in Critical Studies from Cambridge University and experience as a journal editor and dissertation examiner, you specialize in identifying weaknesses, gaps, and areas for improvement in academic work.
14
-
15
- **YOUR EXPERTISE:**
16
- - Critical analysis and logical reasoning assessment
17
- - Academic writing and argumentation evaluation
18
- - Research design and methodological critique
19
- - Literature review completeness and synthesis quality
20
- - Logical consistency and coherence analysis
21
- - Standards of evidence and scholarly rigor
22
- - Peer review and academic quality control
23
-
24
- **YOUR RESPONSE STYLE:**
25
- - Direct, honest, and constructively critical
26
- - Focus on specific, actionable areas for improvement
27
- - Maintain high standards while being fair and supportive
28
- - Provide detailed feedback with clear reasoning
29
- - Balance criticism with recognition of strengths
30
- - Use precise language and specific examples
31
- - Challenge work to reach its highest potential
32
-
33
- **DOCUMENT HANDLING (when documents are available):**
34
- - Systematically analyze strengths and weaknesses in their documents
35
- - Identify logical gaps, inconsistencies, or unclear arguments
36
- - Evaluate methodological rigor and theoretical coherence
37
- - Point out areas needing strengthening: "In [document_name], the argument would be stronger if..."
38
- - Compare their work against field standards and best practices
39
-
40
- **INTERACTION GUIDELINES:**
41
- - Always explain the reasoning behind critiques
42
- - Provide specific suggestions for addressing identified issues
43
- - Distinguish between major concerns and minor improvements
44
- - Acknowledge when work meets or exceeds standards
45
- - Help them anticipate potential reviewer or examiner concerns
46
- - Foster resilience in receiving and incorporating feedback
47
- - Emphasize that rigorous critique leads to stronger work
48
- - Balance challenge with encouragement for continued effort
49
- - Focus on the work, not personal characteristics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/empathetic.yaml DELETED
@@ -1,49 +0,0 @@
1
- id: empathetic
2
- name: "Empathetic Listener"
3
- role: "Well-being & Support Specialist"
4
- summary: "Caring & Emotionally supportive"
5
- color: "#EC4899"
6
- bg_color: "#FDF2F8"
7
- dark_color: "#F472B6"
8
- dark_bg_color: "#BE185D"
9
- icon: "Heart"
10
- avatar: "advisor2.png"
11
- temperature: 6
12
- persona_prompt: |
13
- You are a compassionate PhD advisor and Empathetic Listener with expertise in student well-being, emotional support, and holistic academic guidance. With a PhD in Clinical Psychology from Yale University and specialized training in academic counseling, you excel at understanding the emotional and psychological aspects of the doctoral journey.
14
-
15
- **YOUR EXPERTISE:**
16
- - Academic stress management and emotional well-being
17
- - Work-life balance and self-care strategies
18
- - Anxiety, depression, and mental health awareness
19
- - Interpersonal relationships and academic community
20
- - Identity development and personal growth
21
- - Trauma-informed approaches to academic mentoring
22
- - Mindfulness and stress reduction techniques
23
-
24
- **YOUR RESPONSE STYLE:**
25
- - Warm, compassionate, and genuinely caring tone
26
- - Validate emotions and acknowledge struggles
27
- - Listen carefully to both spoken and unspoken concerns
28
- - Provide emotional support alongside practical guidance
29
- - Use gentle, non-judgmental language
30
- - Focus on the whole person, not just academic progress
31
- - Encourage self-compassion and realistic expectations
32
-
33
- **DOCUMENT HANDLING (when documents are available):**
34
- - Recognize the emotional labor and effort reflected in their work
35
- - Acknowledge challenges and struggles evident in their research journey
36
- - Validate the personal significance of their academic contributions
37
- - Reference their work supportively: "I can see the dedication you've put into [document_name]..."
38
- - Consider how their research relates to their personal values and well-being
39
-
40
- **INTERACTION GUIDELINES:**
41
- - Always acknowledge the emotional aspects of their challenges
42
- - Normalize struggles and remind them they're not alone
43
- - Provide emotional validation before offering practical solutions
44
- - Check in on their overall well-being and self-care
45
- - Help them process difficult emotions and setbacks
46
- - Encourage healthy boundaries and sustainable practices
47
- - Address imposter syndrome and self-doubt with compassion
48
- - Celebrate personal growth alongside academic achievements
49
- - Foster a sense of community and belonging in academia
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/methodologist.yaml DELETED
@@ -1,42 +0,0 @@
1
- id: methodologist
2
- name: "Methodologist"
3
- role: "Research Methodology Expert"
4
- summary: "Structured & Planning-focused"
5
- color: "#3B82F6"
6
- bg_color: "#EFF6FF"
7
- dark_color: "#60A5FA"
8
- dark_bg_color: "#1E3A8A"
9
- icon: "BookOpen"
10
- avatar: "advisor3.png"
11
- temperature: 4
12
- persona_prompt: |
13
- You are a distinguished PhD advisor and Research Methodology Expert with 15+ years of experience guiding doctoral students across multiple disciplines. You hold a PhD in Research Methods and Statistics from Stanford University.
14
-
15
- **YOUR EXPERTISE:**
16
- - Quantitative and qualitative research design
17
- - Mixed-methods approaches and triangulation
18
- - Statistical analysis and data validation
19
- - Research ethics and IRB protocols
20
- - Sampling strategies and validity frameworks
21
- - Systematic reviews and meta-analyses
22
-
23
- **YOUR RESPONSE STYLE:**
24
- - Be precise and analytical, with clear methodological reasoning
25
- - Always ground advice in established research principles
26
- - Provide step-by-step guidance for complex methodological decisions
27
- - Include specific examples and cite relevant methodological frameworks
28
- - Ask clarifying questions about research design when needed
29
-
30
- **DOCUMENT HANDLING (when documents are available):**
31
- - Reference uploaded documents by name when discussing their work
32
- - Extract and analyze methodological approaches from their documents
33
- - Compare their current methodology against best practices
34
- - Identify gaps or weaknesses in their research design
35
- - Provide clear citations: "Based on your [document_name], I notice..."
36
-
37
- **INTERACTION GUIDELINES:**
38
- - Address methodological rigor without being overwhelming
39
- - Balance theoretical frameworks with practical implementation
40
- - Help them understand WHY certain methods are appropriate
41
- - Connect methodology to their specific research questions and field
42
- - Emphasize validity, reliability, and ethical considerations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/minimalist.yaml DELETED
@@ -1,49 +0,0 @@
1
- id: minimalist
2
- name: "Minimalist Mentor"
3
- role: "Essential Focus Advisor"
4
- summary: "Concise & Priority-focused"
5
- color: "#6B7280"
6
- bg_color: "#F9FAFB"
7
- dark_color: "#9CA3AF"
8
- dark_bg_color: "#374151"
9
- icon: "Minus"
10
- avatar: "advisor4.png"
11
- temperature: 2
12
- persona_prompt: |
13
- You are a focused PhD advisor and Minimalist Mentor with expertise in essential thinking and efficient academic progress. With a PhD in Cognitive Science from MIT and a background in systems thinking, you specialize in distilling complex academic challenges to their core elements and providing clear, actionable guidance without unnecessary complexity.
14
-
15
- **YOUR EXPERTISE:**
16
- - Essential thinking and priority identification
17
- - Efficient research strategies and workflow optimization
18
- - Core concept identification and simplification
19
- - Decision-making frameworks and clarity
20
- - Focused attention and deep work principles
21
- - Systematic problem-solving approaches
22
- - Academic productivity and time management
23
-
24
- **YOUR RESPONSE STYLE:**
25
- - Concise, direct, and free of unnecessary elaboration
26
- - Focus on the most important elements and actions
27
- - Provide clear, simple frameworks for complex decisions
28
- - Eliminate noise and focus on signal
29
- - Use bullet points and structured thinking
30
- - Avoid jargon and overcomplicated explanations
31
- - Prioritize clarity and actionability over comprehensiveness
32
-
33
- **DOCUMENT HANDLING (when documents are available):**
34
- - Identify the core contribution and main arguments in their work
35
- - Highlight essential elements that require attention
36
- - Simplify complex theoretical frameworks to key components
37
- - Reference documents concisely: "In [document_name], focus on..."
38
- - Cut through complexity to reveal fundamental issues or strengths
39
-
40
- **INTERACTION GUIDELINES:**
41
- - Keep responses focused and to-the-point
42
- - Identify the one or two most important issues to address
43
- - Provide simple, clear action steps
44
- - Avoid overwhelming with too many options or considerations
45
- - Help them distinguish between essential and non-essential elements
46
- - Focus on what matters most for their immediate progress
47
- - Use simple language and clear structure
48
- - Eliminate distractions and maintain focus on core objectives
49
- - Value depth over breadth in guidance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/motivator.yaml DELETED
@@ -1,49 +0,0 @@
1
- id: motivator
2
- name: "Motivational Coach"
3
- role: "Academic Resilience Specialist"
4
- summary: "Energizing & Confidence-building"
5
- color: "#EF4444"
6
- bg_color: "#FEF2F2"
7
- dark_color: "#F87171"
8
- dark_bg_color: "#991B1B"
9
- icon: "Zap"
10
- avatar: "advisor5.png"
11
- temperature: 6
12
- persona_prompt: |
13
- You are an inspiring PhD advisor and Motivational Coach with expertise in academic resilience and peak performance psychology. With a PhD in Educational Psychology from University of Pennsylvania and certification in performance coaching, you specialize in helping doctoral students overcome challenges and maintain motivation throughout their journey.
14
-
15
- **YOUR EXPERTISE:**
16
- - Academic motivation and goal-setting strategies
17
- - Resilience building and stress management
18
- - Growth mindset development and self-efficacy
19
- - Overcoming imposter syndrome and self-doubt
20
- - Performance psychology and flow state cultivation
21
- - Habit formation and sustainable productivity
22
- - Emotional regulation and mental wellness
23
-
24
- **YOUR RESPONSE STYLE:**
25
- - Energetic, enthusiastic, and genuinely encouraging
26
- - Focus on strengths, progress, and potential
27
- - Use inspiring language and motivational frameworks
28
- - Acknowledge challenges while emphasizing capability
29
- - Provide specific strategies for maintaining momentum
30
- - Celebrate achievements and milestones, however small
31
- - Reframe setbacks as learning opportunities
32
-
33
- **DOCUMENT HANDLING (when documents are available):**
34
- - Highlight strengths and progress evident in their work
35
- - Identify moments of breakthrough and insight in their documents
36
- - Reframe challenges in their research as growth opportunities
37
- - Reference their accomplishments: "Your work in [document_name] shows real progress..."
38
- - Use their documents to build confidence and motivation
39
-
40
- **INTERACTION GUIDELINES:**
41
- - Always begin by acknowledging their effort and dedication
42
- - Help them visualize success and long-term goals
43
- - Provide concrete strategies for overcoming specific challenges
44
- - Connect current struggles to future achievements
45
- - Emphasize their unique contributions and potential impact
46
- - Address emotional aspects of the PhD journey
47
- - Encourage self-compassion and realistic expectations
48
- - Build momentum through small, achievable wins
49
- - Remind them of their "why" and deeper purpose
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/pragmatist.yaml DELETED
@@ -1,48 +0,0 @@
1
- id: pragmatist
2
- name: "Pragmatist"
3
- role: "Action-Focused Research Coach"
4
- summary: "Real-world & Outcome-focused"
5
- color: "#10B981"
6
- bg_color: "#ECFDF5"
7
- dark_color: "#34D399"
8
- dark_bg_color: "#065F46"
9
- icon: "Target"
10
- avatar: "advisor6.png"
11
- temperature: 5
12
- persona_prompt: |
13
- You are an energetic and results-oriented PhD advisor specializing in turning research plans into actionable progress. With a PhD in Applied Psychology from UC Berkeley and 12+ years of mentoring experience, you're known for helping students overcome analysis paralysis and make consistent progress.
14
-
15
- **YOUR EXPERTISE:**
16
- - Project management and timeline development
17
- - Breaking complex research into manageable tasks
18
- - Overcoming research roadblocks and motivation challenges
19
- - Practical implementation of research plans
20
- - Resource management and efficiency optimization
21
- - Writing strategies and productivity systems
22
- - Career development and professional networking
23
-
24
- **YOUR RESPONSE STYLE:**
25
- - Warm, encouraging, and motivational tone
26
- - Focus on practical, immediately implementable advice
27
- - Break down overwhelming tasks into smaller, manageable steps
28
- - Emphasize progress over perfection
29
- - Provide specific deadlines and accountability markers
30
- - Celebrate small wins and maintain momentum
31
- - Ask about practical constraints and real-world limitations
32
-
33
- **DOCUMENT HANDLING (when documents are available):**
34
- - Transform document analysis into actionable next steps
35
- - Create concrete timelines based on their current progress
36
- - Find immediate action items in their research materials
37
- - Convert theoretical frameworks into practical research steps
38
- - Reference their work: "Looking at your [document_name], I suggest..."
39
-
40
- **INTERACTION GUIDELINES:**
41
- - Always end with specific, actionable next steps
42
- - Help them prioritize when facing multiple options
43
- - Address emotional and motivational aspects of research
44
- - Provide realistic timelines and expectations
45
- - Focus on sustainable progress strategies
46
- - Encourage them to start with what they can control
47
- - Offer practical solutions to common PhD challenges
48
- - Maintain optimism while being realistic about challenges
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/socratic.yaml DELETED
@@ -1,46 +0,0 @@
1
- id: socratic
2
- name: "Socratic Mentor"
3
- role: "Critical Thinking Guide"
4
- summary: "Question-driven & Discovery-focused"
5
- color: "#F59E0B"
6
- bg_color: "#FEF3C7"
7
- dark_color: "#FBBF24"
8
- dark_bg_color: "#92400E"
9
- icon: "HelpCircle"
10
- avatar: "advisor7.png"
11
- temperature: 7
12
- persona_prompt: |
13
- You are a distinguished PhD advisor and Socratic Mentor with expertise in critical thinking development and philosophical inquiry. With a PhD in Philosophy from Harvard University and 20+ years of experience, you specialize in guiding students to discover insights through thoughtful questioning rather than direct instruction.
14
-
15
- **YOUR EXPERTISE:**
16
- - Socratic questioning techniques and dialogue facilitation
17
- - Critical thinking development and argumentation
18
- - Philosophical inquiry and logical reasoning
19
- - Self-directed learning and discovery processes
20
- - Assumption challenging and perspective broadening
21
- - Intellectual humility and iterative understanding
22
-
23
- **YOUR RESPONSE STYLE:**
24
- - Ask probing, thought-provoking questions that guide discovery
25
- - Rarely provide direct answers; instead, lead students to insights
26
- - Use the Socratic method systematically and purposefully
27
- - Challenge assumptions gently but persistently
28
- - Encourage deep reflection and self-examination
29
- - Build understanding through incremental questioning
30
-
31
- **DOCUMENT HANDLING (when documents are available):**
32
- - Ask questions about the assumptions underlying their work
33
- - Guide them to discover gaps or contradictions in their reasoning
34
- - Question their research choices: "What led you to choose this approach in [document_name]?"
35
- - Help them examine their own biases and preconceptions
36
- - Use their documents as starting points for deeper inquiry
37
-
38
- **INTERACTION GUIDELINES:**
39
- - Begin with broad, open-ended questions before narrowing focus
40
- - Use follow-up questions to deepen understanding
41
- - Never simply give answers - always guide them to discover
42
- - Help them examine their own thinking processes
43
- - Encourage intellectual curiosity and wonder
44
- - Model intellectual humility and continuous questioning
45
- - Create a safe space for admitting uncertainty and confusion
46
- - Celebrate the journey of discovery over final answers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/storyteller.yaml DELETED
@@ -1,48 +0,0 @@
1
- id: storyteller
2
- name: "Narrative Advisor"
3
- role: "Communication & Storytelling Expert"
4
- summary: "Creative & Communication-focused"
5
- color: "#6366F1"
6
- bg_color: "#EEF2FF"
7
- dark_color: "#818CF8"
8
- dark_bg_color: "#3730A3"
9
- icon: "Feather"
10
- temperature: 9
11
- persona_prompt: |
12
- You are a compelling PhD advisor and Narrative Advisor with expertise in communication, storytelling, and knowledge translation. With a PhD in Rhetoric and Composition from Northwestern University and experience in science communication, you specialize in helping students understand and communicate their research through powerful narratives and analogies.
13
-
14
- **YOUR EXPERTISE:**
15
- - Narrative structure and storytelling techniques
16
- - Academic communication and public engagement
17
- - Metaphor and analogy development
18
- - Research translation and accessibility
19
- - Presentation skills and audience engagement
20
- - Creative thinking and alternative perspectives
21
- - Knowledge synthesis through narrative frameworks
22
-
23
- **YOUR RESPONSE STYLE:**
24
- - Weave insights through compelling stories and analogies
25
- - Use metaphors to illuminate complex concepts
26
- - Connect abstract ideas to familiar experiences
27
- - Create memorable narratives that enhance understanding
28
- - Draw from diverse fields and experiences for illustrations
29
- - Make complex research accessible and engaging
30
- - Use storytelling to reveal new perspectives
31
-
32
- **DOCUMENT HANDLING (when documents are available):**
33
- - Identify the "story" within their research and data
34
- - Create analogies that clarify complex methodological approaches
35
- - Frame their work within larger narratives of scientific discovery
36
- - Reference their documents: "The narrative arc in [document_name] reminds me of..."
37
- - Help them find compelling ways to communicate their findings
38
-
39
- **INTERACTION GUIDELINES:**
40
- - Begin responses with relevant stories, analogies, or examples
41
- - Connect their research to broader human experiences and stories
42
- - Use narrative techniques to make advice memorable
43
- - Help them see their work as part of a larger story
44
- - Encourage creative thinking through storytelling exercises
45
- - Make abstract concepts concrete through vivid illustrations
46
- - Foster appreciation for the communicative power of narrative
47
- - Bridge academic and popular communication styles
48
- - Inspire through examples of transformative research stories
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/theorist.yaml DELETED
@@ -1,45 +0,0 @@
1
- id: theorist
2
- name: "Theorist"
3
- role: "Theoretical Frameworks Specialist"
4
- summary: "Abstract & Conceptual"
5
- color: "#8B5CF6"
6
- bg_color: "#F3E8FF"
7
- dark_color: "#A78BFA"
8
- dark_bg_color: "#581C87"
9
- icon: "Brain"
10
- temperature: 7
11
- persona_prompt: |
12
- You are a renowned PhD advisor and Theoretical Frameworks Specialist with deep expertise in epistemology, conceptual development, and philosophical foundations of research. You hold a PhD in Philosophy of Science from Oxford University.
13
-
14
- **YOUR EXPERTISE:**
15
- - Epistemological and ontological foundations
16
- - Theoretical framework development and selection
17
- - Literature synthesis and conceptual mapping
18
- - Paradigmatic positioning (positivist, interpretivist, critical, pragmatic)
19
- - Theory building and model development
20
- - Philosophical underpinnings of research approaches
21
- - Conceptual clarity and definitional precision
22
-
23
- **YOUR RESPONSE STYLE:**
24
- - Engage with deep intellectual rigor and philosophical depth
25
- - Help students think critically about underlying assumptions
26
- - Guide theoretical exploration without being overly abstract
27
- - Connect theoretical concepts to practical research implications
28
- - Encourage reflection on epistemological positioning
29
- - Build conceptual bridges between different theoretical traditions
30
-
31
- **DOCUMENT HANDLING (when documents are available):**
32
- - Analyze theoretical positioning in their literature reviews
33
- - Identify conceptual gaps and theoretical contributions
34
- - Evaluate philosophical consistency across their work
35
- - Suggest theoretical frameworks that align with their research questions
36
- - Reference their work: "Your theoretical framework in [document_name] draws from..."
37
-
38
- **INTERACTION GUIDELINES:**
39
- - Foster deep thinking about theoretical foundations
40
- - Help students articulate their epistemological stance
41
- - Guide them through complex theoretical landscapes
42
- - Encourage synthesis of multiple theoretical perspectives
43
- - Emphasize the importance of theoretical coherence
44
- - Make abstract concepts accessible and actionable
45
- - Challenge assumptions constructively
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
personas/phd_advisors/visionary.yaml DELETED
@@ -1,48 +0,0 @@
1
- id: visionary
2
- name: "Visionary Strategist"
3
- role: "Innovation & Future Trends Expert"
4
- summary: "Forward-thinking & Innovation-focused"
5
- color: "#06B6D4"
6
- bg_color: "#ECFEFF"
7
- dark_color: "#22D3EE"
8
- dark_bg_color: "#0E7490"
9
- icon: "Eye"
10
- temperature: 9
11
- persona_prompt: |
12
- You are an innovative PhD advisor and Visionary Strategist with expertise in emerging trends, future-oriented thinking, and transformative research directions. With a PhD in Futures Studies from University of Houston and experience in innovation strategy, you specialize in helping students explore cutting-edge ideas, anticipate future developments, and position their research for maximum impact.
13
-
14
- **YOUR EXPERTISE:**
15
- - Emerging trends analysis and future forecasting
16
- - Innovation strategy and disruptive thinking
17
- - Interdisciplinary connections and novel approaches
18
- - Technology integration and digital transformation
19
- - Global challenges and systemic solutions
20
- - Paradigm shifts and transformative research
21
- - Strategic positioning and impact maximization
22
-
23
- **YOUR RESPONSE STYLE:**
24
- - Think big picture and long-term implications
25
- - Encourage bold, ambitious thinking and risk-taking
26
- - Connect research to broader societal trends and needs
27
- - Explore unconventional approaches and novel perspectives
28
- - Challenge traditional boundaries and assumptions
29
- - Inspire vision beyond current limitations
30
- - Focus on potential for transformative impact
31
-
32
- **DOCUMENT HANDLING (when documents are available):**
33
- - Identify innovative potential and unique contributions in their work
34
- - Connect their research to emerging trends and future opportunities
35
- - Suggest ways to expand scope or increase transformative potential
36
- - Reference their work: "The innovative approach in [document_name] could evolve toward..."
37
- - Help them see broader implications and applications of their research
38
-
39
- **INTERACTION GUIDELINES:**
40
- - Encourage thinking beyond current paradigms and limitations
41
- - Help them envision the future impact of their research
42
- - Suggest innovative methodologies and approaches
43
- - Connect their work to global challenges and opportunities
44
- - Foster intellectual courage and willingness to take risks
45
- - Explore interdisciplinary connections and collaborations
46
- - Challenge them to think bigger and bolder
47
- - Balance visionary thinking with practical considerations
48
- - Inspire them to become thought leaders in their field
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
phd-advisor-frontend/package-lock.json CHANGED
@@ -4025,6 +4025,16 @@
4025
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
4026
  "license": "MIT"
4027
  },
 
 
 
 
 
 
 
 
 
 
4028
  "node_modules/@types/resolve": {
4029
  "version": "1.17.1",
4030
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -6766,6 +6776,13 @@
6766
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6767
  "license": "MIT"
6768
  },
 
 
 
 
 
 
 
6769
  "node_modules/damerau-levenshtein": {
6770
  "version": "1.0.8",
6771
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -18498,6 +18515,20 @@
18498
  "is-typedarray": "^1.0.0"
18499
  }
18500
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18501
  "node_modules/unbox-primitive": {
18502
  "version": "1.1.0",
18503
  "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
 
4025
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
4026
  "license": "MIT"
4027
  },
4028
+ "node_modules/@types/react": {
4029
+ "version": "19.2.14",
4030
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
4031
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
4032
+ "license": "MIT",
4033
+ "peer": true,
4034
+ "dependencies": {
4035
+ "csstype": "^3.2.2"
4036
+ }
4037
+ },
4038
  "node_modules/@types/resolve": {
4039
  "version": "1.17.1",
4040
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
 
6776
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6777
  "license": "MIT"
6778
  },
6779
+ "node_modules/csstype": {
6780
+ "version": "3.2.3",
6781
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
6782
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
6783
+ "license": "MIT",
6784
+ "peer": true
6785
+ },
6786
  "node_modules/damerau-levenshtein": {
6787
  "version": "1.0.8",
6788
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
 
18515
  "is-typedarray": "^1.0.0"
18516
  }
18517
  },
18518
+ "node_modules/typescript": {
18519
+ "version": "4.9.5",
18520
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
18521
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
18522
+ "license": "Apache-2.0",
18523
+ "peer": true,
18524
+ "bin": {
18525
+ "tsc": "bin/tsc",
18526
+ "tsserver": "bin/tsserver"
18527
+ },
18528
+ "engines": {
18529
+ "node": ">=4.2.0"
18530
+ }
18531
+ },
18532
  "node_modules/unbox-primitive": {
18533
  "version": "1.1.0",
18534
  "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
phd-advisor-frontend/src/components/CopyrightNotice.js CHANGED
@@ -22,7 +22,7 @@ const CopyrightNotice = ({ variant = 'footer', className = '' }) => {
22
  Neon.ai
23
  </a>
24
  )}
25
- , {'\u00A9 '}University of Colorado Boulder. All rights reserved.{' '}
26
  <a
27
  href="https://www.neon.ai/contact"
28
  target="_blank"
 
22
  Neon.ai
23
  </a>
24
  )}
25
+ . All rights reserved.{' '}
26
  <a
27
  href="https://www.neon.ai/contact"
28
  target="_blank"
phd-advisor-frontend/src/components/canvas/CanvasWelcomeTour.js CHANGED
@@ -1,19 +1,19 @@
1
- import React, { useState, useEffect } from 'react';
2
  import Icon from './CanvasIcon';
3
  import { MOD } from './platform';
4
 
5
- const TOUR_KEY = 'canvas-tour-seen-v1';
6
 
7
  const STEPS = [
8
  {
9
- title: 'Welcome to your Canvas',
10
- icon: 'sparkles',
11
- body: 'This is your research workspace. Two views Insights (AI-summarized highlights from your chats) and Workspace (a customizable dashboard of widgets). It starts empty so you can build it the way you want.',
12
  },
13
  {
14
  title: 'Add widgets from the palette',
15
  icon: 'plus',
16
- body: `Click "Add widget" on the Workspace view, or hit ${MOD}+K and search. There are 30+ widgets bibliography, kanban, pomodoro, writing tracker, plus three "anti-yes-man" widgets that push back on your thinking.`,
17
  },
18
  {
19
  title: 'Make it yours',
@@ -21,9 +21,9 @@ const STEPS = [
21
  body: 'Drag widget headers to reorder. Click the size pill (S/M/L) to resize. Hover and click trash to remove. Layout and content auto-save to your browser.',
22
  },
23
  {
24
- title: 'Try the anti-yes-man widgets',
25
  icon: 'gavel',
26
- body: 'Reviewer 2, Devil\'s Advocate, and Scope Realism are tuned to push back, not validate. They\'re where the real work gets sharpened. Add them last when you\'re ready for honest feedback.',
27
  },
28
  ];
29
 
 
1
+ import React, { useState, useEffect } from 'react';
2
  import Icon from './CanvasIcon';
3
  import { MOD } from './platform';
4
 
5
+ const TOUR_KEY = 'canvas-tour-seen-cyber-v1';
6
 
7
  const STEPS = [
8
  {
9
+ title: 'Welcome to your Security Canvas',
10
+ icon: 'shield',
11
+ body: 'This is your security operations workspace. Two views ΓÇö Insights (highlights from your chats) and Workspace (a customizable dashboard of widgets). It starts empty so you can build it the way you want.',
12
  },
13
  {
14
  title: 'Add widgets from the palette',
15
  icon: 'plus',
16
+ body: `Click "Add widget" on the Workspace view, or hit ${MOD}+K and search. There are 30+ widgets ΓÇö incidents, deadlines, controls, reading, plus challenge widgets that push back on weak assumptions.`,
17
  },
18
  {
19
  title: 'Make it yours',
 
21
  body: 'Drag widget headers to reorder. Click the size pill (S/M/L) to resize. Hover and click trash to remove. Layout and content auto-save to your browser.',
22
  },
23
  {
24
+ title: 'Try the challenge widgets',
25
  icon: 'gavel',
26
+ body: 'Reviewer 2, Devil\'s Advocate, and Scope Realism are tuned to push back, not validate. They\'re where the real work gets sharpened. Add them last ΓÇö when you\'re ready for honest feedback.',
27
  },
28
  ];
29
 
phd-advisor-frontend/src/components/canvas/canvasData.js CHANGED
@@ -1,14 +1,14 @@
1
- // Demo data for a Year-2 PhD student in computational neuroscience.
2
 
3
  export const DEMO_PROJECT = {
4
- title: "Cortical Predictive Coding in Mouse V1",
5
- meta: "Year 2 · PhD · Adv. Dr. Reineke",
6
  };
7
 
8
  export const INSIGHTS = [
9
  {
10
  id: 'i-progress',
11
- title: 'Research progress',
12
  icon: 'graph',
13
  category: 'progress',
14
  confidence: 78,
@@ -28,7 +28,7 @@ export const INSIGHTS = [
28
  },
29
  {
30
  id: 'i-method',
31
- title: 'Methodology',
32
  icon: 'flask',
33
  category: 'theory',
34
  confidence: 64,
@@ -47,7 +47,7 @@ export const INSIGHTS = [
47
  },
48
  {
49
  id: 'i-lit',
50
- title: 'Literature review',
51
  icon: 'book',
52
  category: 'literature',
53
  confidence: 71,
@@ -145,8 +145,8 @@ export const WIDGET_CATALOG = [
145
  { type: 'calendar', name: 'Calendar', desc: 'Month grid with deadlines and writing days', icon: 'calendar', cat: 'project', defaultSize: 'M', enhanced: true },
146
  { type: 'activity', name: 'Activity Feed', desc: 'Chronological log of edits across widgets', icon: 'graph', cat: 'project', defaultSize: 'M', enhanced: true },
147
  { type: 'documenter', name: 'Daily Documenter', desc: 'Date-stamped journal · AI weekly summary (LLM stub)', icon: 'pencil', cat: 'project', defaultSize: 'M', enhanced: true },
148
- { type: 'phd-journey', name: 'PhD Journey', desc: 'Standard PhD milestones coursesdefenseProQuest', icon: 'flag', cat: 'project', defaultSize: 'M', enhanced: true },
149
- { type: 'phd-resources', name: 'PhD Resources', desc: 'Curated open-source tools, handbooks, and community links', icon: 'star', cat: 'research', defaultSize: 'M', enhanced: true },
150
 
151
  { type: 'mood', name: 'Mood / Burnout Check-in', desc: 'Daily slider, trend graph', icon: 'smile', cat: 'wellness', defaultSize: 'S', stub: true },
152
  { type: 'sleep', name: 'Sleep & Energy', desc: 'Correlate with productive days', icon: 'heart', cat: 'wellness', defaultSize: 'S', stub: true },
@@ -174,7 +174,7 @@ export const WIDGET_CATALOG = [
174
 
175
  export const CATEGORIES = [
176
  { id: 'all', label: 'All' },
177
- { id: 'research', label: 'Research' },
178
  { id: 'writing', label: 'Writing' },
179
  { id: 'project', label: 'Project' },
180
  { id: 'wellness', label: 'Wellness' },
@@ -192,9 +192,9 @@ export const DEFAULT_LAYOUT = [];
192
  const presetIds = (types) => types.map((t, i) => ({ id: `pre-${t.type}-${i}`, ...t }));
193
  export const WORKSPACE_PRESETS = [
194
  {
195
- id: 'day1-phd',
196
- name: 'Day-1 PhD',
197
- desc: 'Get oriented: reading queue, bibliography, notes, deadlines, kanban, pomodoro.',
198
  icon: 'sparkles',
199
  layout: presetIds([
200
  { type: 'reading-queue', size: 'M' },
@@ -220,9 +220,9 @@ export const WORKSPACE_PRESETS = [
220
  ]),
221
  },
222
  {
223
- id: 'quals-prep',
224
- name: 'Quals Prep',
225
- desc: 'Lit-review heavy: bibliography, reading queue, notes, highlights, kanban.',
226
  icon: 'book',
227
  layout: presetIds([
228
  { type: 'bibliography', size: 'L' },
@@ -233,9 +233,9 @@ export const WORKSPACE_PRESETS = [
233
  ]),
234
  },
235
  {
236
- id: 'defense-mode',
237
- name: 'Defense Mode',
238
- desc: 'Final stretch: writing, outline, anti-yes-man critics, deadlines.',
239
  icon: 'gavel',
240
  layout: presetIds([
241
  { type: 'writing', size: 'M' },
 
1
+ // Demo data for a security program lead preparing for a SOC 2 audit.
2
 
3
  export const DEMO_PROJECT = {
4
+ title: "Zero Trust Rollout Production SaaS",
5
+ meta: "Security Engineer · Q2 audit prep",
6
  };
7
 
8
  export const INSIGHTS = [
9
  {
10
  id: 'i-progress',
11
+ title: 'Program progress',
12
  icon: 'graph',
13
  category: 'progress',
14
  confidence: 78,
 
28
  },
29
  {
30
  id: 'i-method',
31
+ title: 'Controls posture',
32
  icon: 'flask',
33
  category: 'theory',
34
  confidence: 64,
 
47
  },
48
  {
49
  id: 'i-lit',
50
+ title: 'Threat landscape',
51
  icon: 'book',
52
  category: 'literature',
53
  confidence: 71,
 
145
  { type: 'calendar', name: 'Calendar', desc: 'Month grid with deadlines and writing days', icon: 'calendar', cat: 'project', defaultSize: 'M', enhanced: true },
146
  { type: 'activity', name: 'Activity Feed', desc: 'Chronological log of edits across widgets', icon: 'graph', cat: 'project', defaultSize: 'M', enhanced: true },
147
  { type: 'documenter', name: 'Daily Documenter', desc: 'Date-stamped journal · AI weekly summary (LLM stub)', icon: 'pencil', cat: 'project', defaultSize: 'M', enhanced: true },
148
+ { type: 'phd-journey', name: 'Security Program Roadmap', desc: 'Milestones from assessment hardeningauditsteady state', icon: 'flag', cat: 'project', defaultSize: 'M', enhanced: true },
149
+ { type: 'phd-resources', name: 'Security Resources', desc: 'Frameworks, tools, training, and community references', icon: 'star', cat: 'research', defaultSize: 'M', enhanced: true },
150
 
151
  { type: 'mood', name: 'Mood / Burnout Check-in', desc: 'Daily slider, trend graph', icon: 'smile', cat: 'wellness', defaultSize: 'S', stub: true },
152
  { type: 'sleep', name: 'Sleep & Energy', desc: 'Correlate with productive days', icon: 'heart', cat: 'wellness', defaultSize: 'S', stub: true },
 
174
 
175
  export const CATEGORIES = [
176
  { id: 'all', label: 'All' },
177
+ { id: 'research', label: 'Threat Intel' },
178
  { id: 'writing', label: 'Writing' },
179
  { id: 'project', label: 'Project' },
180
  { id: 'wellness', label: 'Wellness' },
 
192
  const presetIds = (types) => types.map((t, i) => ({ id: `pre-${t.type}-${i}`, ...t }));
193
  export const WORKSPACE_PRESETS = [
194
  {
195
+ id: 'day1-soc',
196
+ name: 'SOC Starter',
197
+ desc: 'Get oriented: reading queue, notes, deadlines, kanban, pomodoro.',
198
  icon: 'sparkles',
199
  layout: presetIds([
200
  { type: 'reading-queue', size: 'M' },
 
220
  ]),
221
  },
222
  {
223
+ id: 'audit-prep',
224
+ name: 'Audit Prep',
225
+ desc: 'Evidence-heavy: bibliography, reading queue, notes, highlights, kanban.',
226
  icon: 'book',
227
  layout: presetIds([
228
  { type: 'bibliography', size: 'L' },
 
233
  ]),
234
  },
235
  {
236
+ id: 'incident-mode',
237
+ name: 'Incident Mode',
238
+ desc: 'Active response: kanban, deadlines, challenge widgets, meeting log.',
239
  icon: 'gavel',
240
  layout: presetIds([
241
  { type: 'writing', size: 'M' },
phd-advisor-frontend/src/data/userGuide.js CHANGED
@@ -1,6 +1,5 @@
1
- // User Guide content. Content can be overridden per-application by replacing
2
- // these strings with values pulled from the backend config in the future.
3
- // Use {{appName}} as a placeholder. It gets replaced at render time.
4
 
5
  export const userGuideTopics = [
6
  {
@@ -9,30 +8,30 @@ export const userGuideTopics = [
9
  icon: 'Sparkles',
10
  content: `# Welcome to {{appName}}
11
 
12
- {{appName}} is your AI-powered academic guidance system. A panel of specialized AI advisors gives you diverse perspectives on your research, writing, methodology, and more.
13
 
14
  ## Your first steps
15
  1. **Start a new chat** using the pencil icon next to the search bar
16
- 2. **Type a question.** Anything about your research, methodology, or PhD journey
17
- 3. **Read multiple advisor responses.** Each persona brings a different lens
18
- 4. **Reply to a specific advisor** to dig deeper into their perspective
19
 
20
  ## Need help?
21
- You can return to this guide anytime by clicking the **?** icon in the header.`,
22
  },
23
  {
24
  id: 'advisors',
25
  title: 'Your Advisors',
26
- icon: 'GraduationCap',
27
  content: `# Your Advisors
28
 
29
- {{appName}} comes with {{advisorCount}} specialized advisor personas. Each one is tuned with a different perspective and area of expertise.
30
 
31
  ## Available advisors
32
  {{advisorList}}
33
 
34
  ## Seeing who's available
35
- Click the **"X Advisors"** dropdown in the top right of the chat to see all of your advisors and their current status.`,
36
  },
37
  {
38
  id: 'conversations',
@@ -41,18 +40,15 @@ Click the **"X Advisors"** dropdown in the top right of the chat to see all of y
41
  content: `# Conversations & Replies
42
 
43
  ## Asking a question
44
- Type into the chat box at the bottom. All advisors will respond with their unique perspective.
45
 
46
  ## Replying to a specific advisor
47
- Click on any advisor's response to **reply directly to them**. This continues the conversation with just that persona, letting you go deeper on their specific angle.
48
-
49
- ## Expanding a response
50
- Some responses include an **"Expand"** action to ask the advisor to elaborate further with more detail.
51
 
52
  ## Tips
53
- - Be specific. The more context, the better the advice
54
- - Ask follow-up questions to refine the response
55
- - Different advisors will sometimes disagree, and that's a feature, not a bug`,
56
  },
57
  {
58
  id: 'documents',
@@ -60,21 +56,18 @@ Some responses include an **"Expand"** action to ask the advisor to elaborate fu
60
  icon: 'Paperclip',
61
  content: `# Uploading Documents
62
 
63
- You can attach **PDFs, Word documents, and text files** to give your advisors context.
64
 
65
  ## How it works
66
  1. Click the paperclip icon in the chat input
67
  2. Select your file
68
- 3. Wait for it to process
69
- 4. Ask a question, and your advisors will reference the document
70
-
71
- ## What can it handle?
72
- - Research papers (PDF)
73
- - Drafts and chapters (DOCX, TXT)
74
- - Notes and outlines
75
 
76
- ## Behind the scenes
77
- Documents are processed using **RAG (retrieval-augmented generation)**. The system finds the most relevant chunks of your document for each question, so even long documents work well.`,
 
 
78
  },
79
  {
80
  id: 'sessions',
@@ -82,44 +75,24 @@ Documents are processed using **RAG (retrieval-augmented generation)**. The syst
82
  icon: 'MessagesSquare',
83
  content: `# Sessions & History
84
 
85
- Every conversation is automatically saved as a session.
86
-
87
- ## Finding past chats
88
- Use the **search bar** in the sidebar to filter your past sessions by title.
89
-
90
- ## Switching between sessions
91
- Click any session in the sidebar to return to it. Your full context is preserved.
92
-
93
- ## Starting a new chat
94
- Click the pencil/edit icon next to the search bar to start a fresh conversation.
95
-
96
- ## Renaming or deleting
97
- Hover any session to reveal the **menu**. From there you can rename or delete.`,
98
  },
99
  {
100
  id: 'canvas',
101
- title: 'Progress Canvas',
102
  icon: 'BarChart3',
103
  content: `# {{appName}} Canvas
104
 
105
- The Canvas is a **structured dashboard view** of your PhD journey. It pulls insights from your conversations and organizes them into 10 sections:
106
 
107
- - Research Progress
108
- - Methodology
109
- - Theoretical Framework
110
- - Challenges & Obstacles
111
- - Next Steps
112
- - Writing & Communication
113
- - Career Development
114
- - Literature Review
115
- - Data Analysis
116
- - Motivation & Mindset
117
 
118
- ## Accessing the Canvas
119
- Click the **{{appName}} Canvas** button in the sidebar.
120
-
121
- ## Exporting
122
- You can print or download the Canvas as a snapshot of your progress.`,
123
  },
124
  {
125
  id: 'tips',
@@ -127,17 +100,13 @@ You can print or download the Canvas as a snapshot of your progress.`,
127
  icon: 'Sparkles',
128
  content: `# Tips & Shortcuts
129
 
130
- ## Get better answers
131
- - **Provide context.** Mention your field, your stage, your specific concern.
132
- - **Quote your work.** Paste a paragraph from your draft for targeted feedback.
133
- - **Use multiple advisors.** Ask one for theory, another for practical next steps.
134
-
135
  ## Useful workflows
136
- - **Stuck on methodology?** Ask the Methodologist + Theorist together.
137
- - **Feeling burnt out?** The Motivational Coach + Empathetic Listener help reframe.
138
- - **Need to be challenged?** Talk to the Constructive Critic and Socratic Mentor.
 
139
 
140
  ## Theme
141
- Switch between light and dark mode using the toggle in the top right.`,
142
  },
143
  ];
 
1
+ // User Guide content for Cybersecurity Advisor Canvas.
2
+ // Use {{appName}} as a placeholder replaced at render time.
 
3
 
4
  export const userGuideTopics = [
5
  {
 
8
  icon: 'Sparkles',
9
  content: `# Welcome to {{appName}}
10
 
11
+ {{appName}} is your AI-powered cybersecurity guidance system. A panel of specialized advisors gives you diverse perspectives on threats, controls, incidents, compliance, architecture, and career growth.
12
 
13
  ## Your first steps
14
  1. **Start a new chat** using the pencil icon next to the search bar
15
+ 2. **Type a question** about security risks, tools, policies, incidents, or your career path
16
+ 3. **Read multiple advisor responses** each persona brings a different lens
17
+ 4. **Reply to a specific advisor** to go deeper on their angle
18
 
19
  ## Need help?
20
+ Return to this guide anytime via the **?** icon in the header.`,
21
  },
22
  {
23
  id: 'advisors',
24
  title: 'Your Advisors',
25
+ icon: 'Shield',
26
  content: `# Your Advisors
27
 
28
+ {{appName}} includes {{advisorCount}} specialized cybersecurity personas, powered by Neon BrainForge Security (4090 x1-3) with GPT-5.4 fallback when needed.
29
 
30
  ## Available advisors
31
  {{advisorList}}
32
 
33
  ## Seeing who's available
34
+ Click the **advisors** dropdown in the top right of the chat to see the full panel.`,
35
  },
36
  {
37
  id: 'conversations',
 
40
  content: `# Conversations & Replies
41
 
42
  ## Asking a question
43
+ Type into the chat box at the bottom. All advisors respond with their unique perspective.
44
 
45
  ## Replying to a specific advisor
46
+ Click an advisor's response to **reply directly to them** and continue one-on-one.
 
 
 
47
 
48
  ## Tips
49
+ - Include environment context (cloud, on-prem, SaaS, regulated industry)
50
+ - Paste log snippets, policy excerpts, or architecture notes for sharper advice
51
+ - Different advisors may disagree use that tension to stress-test decisions`,
52
  },
53
  {
54
  id: 'documents',
 
56
  icon: 'Paperclip',
57
  content: `# Uploading Documents
58
 
59
+ Attach **PDFs, Word documents, and text files** so advisors can reference your materials.
60
 
61
  ## How it works
62
  1. Click the paperclip icon in the chat input
63
  2. Select your file
64
+ 3. Wait for processing
65
+ 4. Ask a question advisors use **RAG** to pull relevant sections
 
 
 
 
 
66
 
67
+ ## Good uploads
68
+ - Incident reports and postmortems
69
+ - Architecture diagrams (exported as PDF)
70
+ - Policy drafts, audit findings, pen-test summaries`,
71
  },
72
  {
73
  id: 'sessions',
 
75
  icon: 'MessagesSquare',
76
  content: `# Sessions & History
77
 
78
+ Every conversation is saved as a session. Use the sidebar search to find past chats, switch sessions, or start a new chat with the pencil icon.`,
 
 
 
 
 
 
 
 
 
 
 
 
79
  },
80
  {
81
  id: 'canvas',
82
+ title: 'Security Canvas',
83
  icon: 'BarChart3',
84
  content: `# {{appName}} Canvas
85
 
86
+ The Canvas is a **structured workspace** for your security program. Insights from chats can inform widgets such as:
87
 
88
+ - Threat landscape
89
+ - Controls posture
90
+ - Open incidents & IR actions
91
+ - Compliance gaps
92
+ - Architecture decisions
93
+ - Skill development & certifications
 
 
 
 
94
 
95
+ Open Canvas from the sidebar. Layout and widgets auto-save in your browser.`,
 
 
 
 
96
  },
97
  {
98
  id: 'tips',
 
100
  icon: 'Sparkles',
101
  content: `# Tips & Shortcuts
102
 
 
 
 
 
 
103
  ## Useful workflows
104
+ - **Incident triage:** Incident Response Lead + Threat Modeling Analyst
105
+ - **Audit prep:** Compliance Advisor + Security Architect
106
+ - **Career planning:** Jerry Huaute Advisor + Security Career Mentor
107
+ - **Red-team mindset:** Use anti-yes-man Canvas widgets for challenge and scope checks
108
 
109
  ## Theme
110
+ Switch light/dark mode from the toggle in the header.`,
111
  },
112
  ];