Spaces:
Running
Running
NeonClary Cursor commited on
Commit ·
cc7440f
1
Parent(s): 294f234
Add Cybersecurity Advisor Canvas on FEAT_CybersecurityCanvas
Browse filesRebrand 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>
- cybersecurity_config.yaml +173 -0
- docker-compose.yml +9 -4
- multi_llm_chatbot_backend/.env.example +12 -0
- multi_llm_chatbot_backend/app/config.py +17 -0
- multi_llm_chatbot_backend/app/core/bootstrap.py +87 -16
- multi_llm_chatbot_backend/app/core/improved_orchestrator.py +7 -9
- multi_llm_chatbot_backend/app/llm/improved_vllm_client.py +49 -9
- multi_llm_chatbot_backend/app/llm/neon_pile.py +67 -0
- multi_llm_chatbot_backend/app/llm/openai_fallback_client.py +140 -0
- multi_llm_chatbot_backend/app/llm/resilient_client.py +130 -0
- multi_llm_chatbot_backend/app/models/default_personas.py +21 -2
- multi_llm_chatbot_backend/app/tests/unit/test_course_search_tool.py +0 -33
- multi_llm_chatbot_backend/app/tests/unit/test_current_datetime_tool.py +21 -0
- multi_llm_chatbot_backend/app/tests/unit/test_resilient_client.py +49 -0
- multi_llm_chatbot_backend/app/tests/unit/test_rmp_tool.py +0 -169
- multi_llm_chatbot_backend/app/tests/unit/test_tool_registry.py +3 -82
- multi_llm_chatbot_backend/app/tools/current_datetime.py +62 -0
- multi_llm_chatbot_backend/app/tools/rate_my_professor.py +0 -202
- multi_llm_chatbot_backend/app/tools/search_courses.py +0 -191
- multi_llm_chatbot_backend/requirements.txt +1 -0
- personas/cyber_advisors/compliance_officer.yaml +19 -0
- personas/cyber_advisors/incident_responder.yaml +19 -0
- personas/cyber_advisors/jerry_huaute.yaml +20 -0
- personas/cyber_advisors/security_architect.yaml +19 -0
- personas/cyber_advisors/security_mentor.yaml +19 -0
- personas/cyber_advisors/threat_modeler.yaml +19 -0
- personas/phd_advisors/critic.yaml +0 -49
- personas/phd_advisors/empathetic.yaml +0 -49
- personas/phd_advisors/methodologist.yaml +0 -42
- personas/phd_advisors/minimalist.yaml +0 -49
- personas/phd_advisors/motivator.yaml +0 -49
- personas/phd_advisors/pragmatist.yaml +0 -48
- personas/phd_advisors/socratic.yaml +0 -46
- personas/phd_advisors/storyteller.yaml +0 -48
- personas/phd_advisors/theorist.yaml +0 -45
- personas/phd_advisors/visionary.yaml +0 -48
- phd-advisor-frontend/package-lock.json +31 -0
- phd-advisor-frontend/src/components/CopyrightNotice.js +1 -1
- phd-advisor-frontend/src/components/canvas/CanvasWelcomeTour.js +8 -8
- phd-advisor-frontend/src/components/canvas/canvasData.js +18 -18
- 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:
|
| 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/
|
|
|
|
| 22 |
frontend:
|
| 23 |
build:
|
| 24 |
context: .
|
| 25 |
dockerfile: Dockerfile
|
| 26 |
target: frontend
|
| 27 |
ports:
|
| 28 |
-
- "
|
| 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 = "
|
| 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 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
chat_orchestrator = ImprovedChatOrchestrator(llm_client=llm)
|
| 34 |
|
| 35 |
-
|
|
|
|
| 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 |
-
"
|
| 67 |
-
"
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 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__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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=
|
| 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 |
-
|
| 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 = {"
|
| 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=["
|
| 60 |
self.assertEqual(len(defs), 1)
|
| 61 |
-
self.assertEqual(defs[0]["function"]["name"], "
|
| 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 |
-
|
| 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: '
|
| 11 |
-
body: 'This is your
|
| 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
|
| 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
|
| 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
|
| 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
|
| 2 |
|
| 3 |
export const DEMO_PROJECT = {
|
| 4 |
-
title: "
|
| 5 |
-
meta: "
|
| 6 |
};
|
| 7 |
|
| 8 |
export const INSIGHTS = [
|
| 9 |
{
|
| 10 |
id: 'i-progress',
|
| 11 |
-
title: '
|
| 12 |
icon: 'graph',
|
| 13 |
category: 'progress',
|
| 14 |
confidence: 78,
|
|
@@ -28,7 +28,7 @@ export const INSIGHTS = [
|
|
| 28 |
},
|
| 29 |
{
|
| 30 |
id: 'i-method',
|
| 31 |
-
title: '
|
| 32 |
icon: 'flask',
|
| 33 |
category: 'theory',
|
| 34 |
confidence: 64,
|
|
@@ -47,7 +47,7 @@ export const INSIGHTS = [
|
|
| 47 |
},
|
| 48 |
{
|
| 49 |
id: 'i-lit',
|
| 50 |
-
title: '
|
| 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: '
|
| 149 |
-
{ type: 'phd-resources', name: '
|
| 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: '
|
| 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-
|
| 196 |
-
name: '
|
| 197 |
-
desc: 'Get oriented: reading queue,
|
| 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: '
|
| 224 |
-
name: '
|
| 225 |
-
desc: '
|
| 226 |
icon: 'book',
|
| 227 |
layout: presetIds([
|
| 228 |
{ type: 'bibliography', size: 'L' },
|
|
@@ -233,9 +233,9 @@ export const WORKSPACE_PRESETS = [
|
|
| 233 |
]),
|
| 234 |
},
|
| 235 |
{
|
| 236 |
-
id: '
|
| 237 |
-
name: '
|
| 238 |
-
desc: '
|
| 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 → hardening → audit → steady 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
|
| 2 |
-
//
|
| 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
|
| 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
|
| 17 |
-
3. **Read multiple advisor responses
|
| 18 |
-
4. **Reply to a specific advisor** to
|
| 19 |
|
| 20 |
## Need help?
|
| 21 |
-
|
| 22 |
},
|
| 23 |
{
|
| 24 |
id: 'advisors',
|
| 25 |
title: 'Your Advisors',
|
| 26 |
-
icon: '
|
| 27 |
content: `# Your Advisors
|
| 28 |
|
| 29 |
-
{{appName}}
|
| 30 |
|
| 31 |
## Available advisors
|
| 32 |
{{advisorList}}
|
| 33 |
|
| 34 |
## Seeing who's available
|
| 35 |
-
Click the **
|
| 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
|
| 45 |
|
| 46 |
## Replying to a specific advisor
|
| 47 |
-
Click
|
| 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 |
-
-
|
| 54 |
-
-
|
| 55 |
-
- Different advisors
|
| 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 |
-
|
| 64 |
|
| 65 |
## How it works
|
| 66 |
1. Click the paperclip icon in the chat input
|
| 67 |
2. Select your file
|
| 68 |
-
3. Wait for
|
| 69 |
-
4. Ask a question
|
| 70 |
-
|
| 71 |
-
## What can it handle?
|
| 72 |
-
- Research papers (PDF)
|
| 73 |
-
- Drafts and chapters (DOCX, TXT)
|
| 74 |
-
- Notes and outlines
|
| 75 |
|
| 76 |
-
##
|
| 77 |
-
|
|
|
|
|
|
|
| 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
|
| 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: '
|
| 102 |
icon: 'BarChart3',
|
| 103 |
content: `# {{appName}} Canvas
|
| 104 |
|
| 105 |
-
The Canvas is a **structured
|
| 106 |
|
| 107 |
-
-
|
| 108 |
-
-
|
| 109 |
-
-
|
| 110 |
-
-
|
| 111 |
-
-
|
| 112 |
-
-
|
| 113 |
-
- Career Development
|
| 114 |
-
- Literature Review
|
| 115 |
-
- Data Analysis
|
| 116 |
-
- Motivation & Mindset
|
| 117 |
|
| 118 |
-
|
| 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 |
-
- **
|
| 137 |
-
- **
|
| 138 |
-
- **
|
|
|
|
| 139 |
|
| 140 |
## Theme
|
| 141 |
-
Switch
|
| 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 |
];
|