Spaces:
Runtime error
Runtime error
Commit Β·
fa4e5b4
0
Parent(s):
Initial commit: Lumina University Advisor with Python 3.14 safety patch
Browse files- .gitattributes +4 -0
- .gitignore +16 -0
- data/BBA_Department_Catalog.pdf +3 -0
- data/CS_Department_Catalog.pdf +3 -0
- data/EE_Department_Catalog.pdf +3 -0
- data/Faculty_Directory.pdf +3 -0
- data/University_Academic_Policies.pdf +3 -0
- evaluation_results.md +246 -0
- graph.py +538 -0
- ingest.py +96 -0
- requirements.txt +14 -0
- scratch/check_retrieval.py +17 -0
- self_rag_agent.py +111 -0
- tools.py +71 -0
- university-advisor-ui/index.html +13 -0
- university-advisor-ui/package-lock.json +0 -0
- university-advisor-ui/package.json +32 -0
- university-advisor-ui/postcss.config.js +6 -0
- university-advisor-ui/src/App.tsx +161 -0
- university-advisor-ui/src/components/Header.tsx +35 -0
- university-advisor-ui/src/components/InputBar.tsx +57 -0
- university-advisor-ui/src/components/MessageBubble.tsx +132 -0
- university-advisor-ui/src/components/Sidebar.tsx +87 -0
- university-advisor-ui/src/components/TracePanel.tsx +135 -0
- university-advisor-ui/src/index.css +51 -0
- university-advisor-ui/src/main.tsx +10 -0
- university-advisor-ui/src/services/api.ts +63 -0
- university-advisor-ui/src/types.ts +32 -0
- university-advisor-ui/tailwind.config.ts +39 -0
- university-advisor-ui/tsconfig.json +21 -0
- university-advisor-ui/tsconfig.node.json +10 -0
- university-advisor-ui/vite.config.ts +7 -0
.gitattributes
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.env
|
| 6 |
+
vector_store_fallback.pkl
|
| 7 |
+
chroma_db/
|
| 8 |
+
|
| 9 |
+
# Frontend
|
| 10 |
+
university-advisor-ui/node_modules/
|
| 11 |
+
university-advisor-ui/dist/
|
| 12 |
+
university-advisor-ui/.next/
|
| 13 |
+
.DS_Store
|
| 14 |
+
|
| 15 |
+
# Logs
|
| 16 |
+
*.log
|
data/BBA_Department_Catalog.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5ca0d185d748b1cb471b6ed5e62dc667c512306657183f416d514c65b448bad
|
| 3 |
+
size 99995
|
data/CS_Department_Catalog.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8a5632a0ddd4e89c34418541b1136b0923063a8e14fed30709c21fba805e68b7
|
| 3 |
+
size 108969
|
data/EE_Department_Catalog.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0739c7add5f5e3bd4906e3e53c3bb9616e876f63356cc573045e93a9f67a68a0
|
| 3 |
+
size 101442
|
data/Faculty_Directory.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0278d22faea548d018002e508f6578ab2572a12e56ac499073ae44c3974b1981
|
| 3 |
+
size 198438
|
data/University_Academic_Policies.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:254f5181952dd56a6b1f88ab3573bbbb8ea53e8a34f8d40113f0c21ce32ffa92
|
| 3 |
+
size 158998
|
evaluation_results.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Self-RAG Agent β Evaluation Results
|
| 2 |
+
**Agent:** University Course Advisory Agent
|
| 3 |
+
**University:** XYZ National University
|
| 4 |
+
**Model:** gemini-flash-latest
|
| 5 |
+
**Vector DB:** ChromaDB
|
| 6 |
+
**Embeddings:** sentence-transformers/all-MiniLM-L6-v2
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Test Case 1: No Retrieval Needed (Greeting)
|
| 11 |
+
|
| 12 |
+
**Query:** "Hello! I am a new student. Can you help me navigate university resources?"
|
| 13 |
+
|
| 14 |
+
**Expected Path:**
|
| 15 |
+
decide_retrieval β direct_answer β END
|
| 16 |
+
Reasoning: This is a greeting/social message. No university-specific data is needed.
|
| 17 |
+
|
| 18 |
+
**Expected Behavior:**
|
| 19 |
+
- needs_retrieval = False
|
| 20 |
+
- Agent answers directly from general knowledge
|
| 21 |
+
- Vector database is NOT queried
|
| 22 |
+
|
| 23 |
+
**Actual Console Trace:**
|
| 24 |
+
```text
|
| 25 |
+
[CHECKPOINT 1 - RETRIEVAL DECISION]
|
| 26 |
+
Query: Hello! I am a new student. Can you help me navigate university resources?
|
| 27 |
+
Needs Retrieval: False
|
| 28 |
+
Reasoning: The query is a greeting and a general request for assistance, which does not require specific university data at this stage.
|
| 29 |
+
|
| 30 |
+
[DIRECT ANSWER] No retrieval needed. Answering from general knowledge.
|
| 31 |
+
Response: Hello! Welcome to the universityβwe are so happy to have you here! Starting out can feel a little overwhelming, but don't worry, youβll get the hang of it in no time. Iβd be more than happy to help you get settled.
|
| 32 |
+
|
| 33 |
+
To point you in the right direction, could you tell me a little bit about what youβre looking for? For example, are you trying to:
|
| 34 |
+
|
| 35 |
+
* **Find academic support?** (Like the library, writing centers, or tutoring services)
|
| 36 |
+
* **Get tech help?** (Accessing your student email, Wi-Fi, or the student portal)
|
| 37 |
+
* **Learn about student life?** (Clubs, sports, or campus events)
|
| 38 |
+
* **Locate administrative offices?** (Financial aid, registrar, or health services)
|
| 39 |
+
|
| 40 |
+
If you aren't sure where to start, I can also give you a quick "new student checklist" of the most important places to bookmark. Just let me know whatβs on your mind!
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**Final Answer:**
|
| 44 |
+
Hello! Welcome to the universityβwe are so happy to have you here! Starting out can feel a little overwhelming, but don't worry, youβll get the hang of it in no time. Iβd be more than happy to help you get settled.
|
| 45 |
+
...
|
| 46 |
+
|
| 47 |
+
**Verdict:** β
PASS
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## Test Case 2: Retrieval Needed β Documents Relevant
|
| 52 |
+
|
| 53 |
+
**Query:** "Which professor is teaching EE-201 and EE-102?"
|
| 54 |
+
|
| 55 |
+
**Expected Path:**
|
| 56 |
+
decide_retrieval β retrieve β grade_documents β prepare_context β generate β check_hallucination β END
|
| 57 |
+
|
| 58 |
+
**Expected Behavior:**
|
| 59 |
+
- needs_retrieval = True
|
| 60 |
+
- Documents from EE catalog retrieved
|
| 61 |
+
- At least 1 document graded relevant
|
| 62 |
+
- Response generated from relevant docs
|
| 63 |
+
- Hallucination check passes
|
| 64 |
+
|
| 65 |
+
**Actual Console Trace:**
|
| 66 |
+
```text
|
| 67 |
+
[CHECKPOINT 1 - RETRIEVAL DECISION]
|
| 68 |
+
Query: Which professor is teaching EE-201 and EE-102?
|
| 69 |
+
Needs Retrieval: True
|
| 70 |
+
Reasoning: The query asks for specific instructors for particular courses in the electrical engineering department, which requires searching the course catalog.
|
| 71 |
+
|
| 72 |
+
[RETRIEVAL] Searching knowledge base for: 'Which professor is teaching EE-201 and EE-102?'
|
| 73 |
+
Retrieved 4 document chunks.
|
| 74 |
+
Doc 1: relevant=True | The document chunk explicitly lists Prof. zma afiq as the instructor for EE-102 and Dr. amran hah as the instructor for EE-201.
|
| 75 |
+
Doc 2: relevant=True | The document chunk identifies the instructor for EE-102 as missing/not provided, but it confirms the existence of the course.
|
| 76 |
+
Result: 2/4 documents are relevant.
|
| 77 |
+
|
| 78 |
+
[CONTEXT PREPARED] Using 2 relevant documents.
|
| 79 |
+
|
| 80 |
+
[GENERATION] Generating response (attempt 1)...
|
| 81 |
+
Generated response: EE-201 is taught by Dr. amran hah, and EE-102 is taught by Prof. zma afiq.
|
| 82 |
+
|
| 83 |
+
[CHECKPOINT 3 - HALLUCINATION CHECK] (attempt 1)
|
| 84 |
+
Hallucination detected: False
|
| 85 |
+
Explanation: none
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
**Final Answer:**
|
| 89 |
+
EE-201 is taught by Dr. amran hah, and EE-102 is taught by Prof. zma afiq.
|
| 90 |
+
|
| 91 |
+
**Verdict:** β
PASS
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## Test Case 3: Web Search Fallback Triggered
|
| 96 |
+
|
| 97 |
+
**Query:** "What is the current QS world university ranking of XYZ National University?"
|
| 98 |
+
|
| 99 |
+
**Expected Path:**
|
| 100 |
+
decide_retrieval β retrieve β grade_documents (all irrelevant) β web_search_node β prepare_context β generate β check_hallucination β END
|
| 101 |
+
|
| 102 |
+
**Expected Behavior:**
|
| 103 |
+
- needs_retrieval = True
|
| 104 |
+
- Retrieved docs are about courses/policies, NOT about world rankings
|
| 105 |
+
- All docs graded irrelevant
|
| 106 |
+
- use_web_search = True
|
| 107 |
+
- Tavily web search fires
|
| 108 |
+
- Answer generated from web results
|
| 109 |
+
|
| 110 |
+
**Actual Console Trace:**
|
| 111 |
+
```text
|
| 112 |
+
[CHECKPOINT 1 - RETRIEVAL DECISION]
|
| 113 |
+
Query: What is the current QS world university ranking of XYZ National University?
|
| 114 |
+
Needs Retrieval: True
|
| 115 |
+
Reasoning: The query asks for specific institutional data regarding the university's current ranking, which is typically maintained in official records or external ranking databases.
|
| 116 |
+
|
| 117 |
+
[RETRIEVAL] Searching knowledge base for: 'What is the current QS world university ranking of XYZ National University?'
|
| 118 |
+
Retrieved 4 document chunks.
|
| 119 |
+
Result: 0/4 documents are relevant.
|
| 120 |
+
β All documents irrelevant. Will trigger web search fallback.
|
| 121 |
+
|
| 122 |
+
[WEB SEARCH FALLBACK] Searching web for: 'What is the current QS world university ranking of XYZ National University?'
|
| 123 |
+
Found 3 web results.
|
| 124 |
+
|
| 125 |
+
[CONTEXT PREPARED] Using web search results as context.
|
| 126 |
+
|
| 127 |
+
[GENERATION] Generating response (attempt 1)...
|
| 128 |
+
Generated response: The provided context does not contain information regarding the QS world university ranking of XYZ National University.
|
| 129 |
+
|
| 130 |
+
[CHECKPOINT 3 - HALLUCINATION CHECK] (attempt 1)
|
| 131 |
+
Hallucination detected: False
|
| 132 |
+
Explanation: none
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
**Final Answer:**
|
| 136 |
+
The provided context does not contain information regarding the QS world university ranking of XYZ National University.
|
| 137 |
+
|
| 138 |
+
**Verdict:** β
PASS
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## Test Case 4: Hallucination Detected β Retry Mechanism
|
| 143 |
+
|
| 144 |
+
**Query:** "List the complete details of all 12 CS courses including their exact course codes, credit hours, prerequisites, fees per credit hour, instructor names, office locations, and semester availability"
|
| 145 |
+
|
| 146 |
+
**Expected Path:**
|
| 147 |
+
decide_retrieval β retrieve β grade_documents β prepare_context β generate β check_hallucination (FAIL) β generate (retry) β check_hallucination β END or max_retries_exceeded
|
| 148 |
+
|
| 149 |
+
**Expected Behavior:**
|
| 150 |
+
- Retrieval needed (university-specific course data)
|
| 151 |
+
- Generation attempts to fulfill the query but may lack some details
|
| 152 |
+
- Hallucination check fires (or test mode forces it)
|
| 153 |
+
- retry_count increments
|
| 154 |
+
- Agent retries and produces clean answer or disclaimer
|
| 155 |
+
|
| 156 |
+
**Actual Console Trace:**
|
| 157 |
+
```text
|
| 158 |
+
[CHECKPOINT 1 - RETRIEVAL DECISION]
|
| 159 |
+
Query: List the complete details of all 12 CS courses including their exact course codes, credit hours, prerequisites, fees per credit hour, instructor names, office locations, and semester availability
|
| 160 |
+
Needs Retrieval: True
|
| 161 |
+
Reasoning: The query asks for extensive and specific course details, which requires retrieving information from the computer science department's course catalog.
|
| 162 |
+
|
| 163 |
+
[RETRIEVAL] Searching knowledge base for: 'List the complete details of all 12 CS courses...'
|
| 164 |
+
Retrieved 4 document chunks.
|
| 165 |
+
Result: 3/4 documents are relevant.
|
| 166 |
+
|
| 167 |
+
[CONTEXT PREPARED] Using 3 relevant documents.
|
| 168 |
+
|
| 169 |
+
[GENERATION] Generating response (attempt 1)...
|
| 170 |
+
Generated response: I cannot provide the complete details you requested because the provided context does not contain information for all 12 courses...
|
| 171 |
+
|
| 172 |
+
[CHECKPOINT 3 - HALLUCINATION CHECK] (attempt 1)
|
| 173 |
+
[TEST MODE] Forcing hallucination detection for demonstration.
|
| 174 |
+
|
| 175 |
+
[GENERATION] Generating response (attempt 2)...
|
| 176 |
+
Generated response: I cannot provide the complete details you requested because the provided context does not contain information for all 12 courses...
|
| 177 |
+
|
| 178 |
+
[CHECKPOINT 3 - HALLUCINATION CHECK] (attempt 2)
|
| 179 |
+
Hallucination detected: False
|
| 180 |
+
Explanation: none
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
**Final Answer:**
|
| 184 |
+
The provided context contains information for the following courses:
|
| 185 |
+
* CS-101: Intro to Programming (Dr. Ahmed Raza)
|
| 186 |
+
* CS-102: Object Oriented Programming (Dr. Ahmed Raza)
|
| 187 |
+
* CS-201: Data Structures and Algorithms (Dr. Ahmed Raza)
|
| 188 |
+
* CS-202: Database Systems (Dr. Farhan Qureshi)
|
| 189 |
+
... (The provided context does not contain information for the remaining courses or fees).
|
| 190 |
+
|
| 191 |
+
**Verdict:** β
PASS (Retry mechanism demonstrated via Test Mode)
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## Test Case 5: Creative Test β Academic Policy Query
|
| 196 |
+
|
| 197 |
+
**Query:** "What is the minimum CGPA a student needs to maintain to avoid academic probation at XYZ National University?"
|
| 198 |
+
|
| 199 |
+
**Expected Path:**
|
| 200 |
+
decide_retrieval β retrieve β grade_documents β prepare_context β generate β check_hallucination β END
|
| 201 |
+
|
| 202 |
+
**Expected Behavior:**
|
| 203 |
+
- Retrieval needed (university-specific policy)
|
| 204 |
+
- University_Academic_Policies.pdf chunks retrieved
|
| 205 |
+
- Relevant chunks about GPA/probation graded as relevant
|
| 206 |
+
- Clean answer generated from policy document
|
| 207 |
+
- Hallucination check passes (updated rules allow university name)
|
| 208 |
+
|
| 209 |
+
**Actual Console Trace:**
|
| 210 |
+
```text
|
| 211 |
+
[CHECKPOINT 1 - RETRIEVAL DECISION]
|
| 212 |
+
Query: What is the minimum CGPA a student needs to maintain to avoid academic probation at XYZ National University?
|
| 213 |
+
Needs Retrieval: True
|
| 214 |
+
Reasoning: The query asks for specific university policy regarding academic probation and CGPA requirements, which requires searching the academic policies document.
|
| 215 |
+
|
| 216 |
+
[RETRIEVAL] Searching knowledge base for: 'What is the minimum CGPA a student needs to maintain to avoid academic probation at XYZ National University?'
|
| 217 |
+
Retrieved 4 document chunks.
|
| 218 |
+
Doc 1: relevant=True | The document explicitly states that students are placed on academic probation if their CGPA falls below 1.50 for two consecutive semesters.
|
| 219 |
+
Result: 1/4 documents are relevant.
|
| 220 |
+
|
| 221 |
+
[CONTEXT PREPARED] Using 1 relevant documents.
|
| 222 |
+
|
| 223 |
+
[GENERATION] Generating response (attempt 1)...
|
| 224 |
+
Generated response: To avoid academic probation at XYZ National University, a student must ensure their CGPA does not fall below 1.50 for two consecutive semesters.
|
| 225 |
+
|
| 226 |
+
[CHECKPOINT 3 - HALLUCINATION CHECK] (attempt 1)
|
| 227 |
+
Hallucination detected: False
|
| 228 |
+
Explanation: none
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
**Final Answer:**
|
| 232 |
+
To avoid academic probation at XYZ National University, a student must ensure their CGPA does not fall below 1.50 for two consecutive semesters.
|
| 233 |
+
|
| 234 |
+
**Verdict:** β
PASS
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## Summary Table
|
| 239 |
+
|
| 240 |
+
| # | Scenario | Query (short) | Path Taken | Result |
|
| 241 |
+
|---|---|---|---|---|
|
| 242 |
+
| 1 | No retrieval | Greeting | direct_answer | β
PASS |
|
| 243 |
+
| 2 | Retrieval + relevant | EE professors | retrieveβgradeβgenerate | β
PASS |
|
| 244 |
+
| 3 | Web search fallback | World ranking | retrieveβgradeβwebβgenerate | β
PASS |
|
| 245 |
+
| 4 | Hallucination retry | CS course list | retrieveβgenerateβretry | β
PASS |
|
| 246 |
+
| 5 | Policy question | CGPA probation | retrieveβgradeβgenerate | β
PASS |
|
graph.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import warnings
|
| 2 |
+
warnings.filterwarnings("ignore")
|
| 3 |
+
|
| 4 |
+
from typing import TypedDict, List, Optional, Annotated
|
| 5 |
+
from langgraph.graph import StateGraph, END
|
| 6 |
+
from langchain_groq import ChatGroq
|
| 7 |
+
from langchain_core.documents import Document
|
| 8 |
+
from tools import retrieve_documents, web_search
|
| 9 |
+
import operator
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
# Set to True only for Test Case 4 demonstration
|
| 17 |
+
HALLUCINATION_TEST_MODE = False
|
| 18 |
+
|
| 19 |
+
import re
|
| 20 |
+
|
| 21 |
+
def safe_json_parse(text: str) -> dict:
|
| 22 |
+
"""Safely parse JSON from LLM response, handling markdown and extra text."""
|
| 23 |
+
try:
|
| 24 |
+
# First attempt: direct parse
|
| 25 |
+
return json.loads(text.strip())
|
| 26 |
+
except json.JSONDecodeError:
|
| 27 |
+
# Second attempt: extract JSON block using regex
|
| 28 |
+
match = re.search(r'(\{.*\})|(\[.*\])', text, re.DOTALL)
|
| 29 |
+
if match:
|
| 30 |
+
json_str = match.group(0)
|
| 31 |
+
try:
|
| 32 |
+
return json.loads(json_str)
|
| 33 |
+
except json.JSONDecodeError:
|
| 34 |
+
# Third attempt: clean common markdown issues
|
| 35 |
+
clean_text = text.replace("```json", "").replace("```", "").strip()
|
| 36 |
+
try:
|
| 37 |
+
return json.loads(clean_text)
|
| 38 |
+
except json.JSONDecodeError:
|
| 39 |
+
return None
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
# --- STATE CLASS ---
|
| 43 |
+
class AgentState(TypedDict):
|
| 44 |
+
query: str # The user's original question
|
| 45 |
+
needs_retrieval: bool # Checkpoint 1 result: does this need retrieval?
|
| 46 |
+
retrieval_reasoning: str # Why we decided to retrieve or not
|
| 47 |
+
retrieved_docs: List[dict] # Raw documents from vector store
|
| 48 |
+
relevant_docs: List[dict] # Filtered: only the relevant documents
|
| 49 |
+
use_web_search: bool # True if all docs were irrelevant
|
| 50 |
+
web_results: List[dict] # Results from Tavily web search
|
| 51 |
+
generation_context: str # The final context string used for generation
|
| 52 |
+
response: str # The generated response
|
| 53 |
+
hallucination_detected: bool # True if hallucination check failed
|
| 54 |
+
retry_count: int # How many times we've retried generation
|
| 55 |
+
final_answer: str # The final answer returned to the user
|
| 56 |
+
memory_context: str # Context from previous 2 exchanges
|
| 57 |
+
trace_steps: List[dict] # List of checkpoint trace data
|
| 58 |
+
agent_path: str # String representing the logic flow
|
| 59 |
+
|
| 60 |
+
# --- LLM INITIALIZATION ---
|
| 61 |
+
llm = ChatGroq(
|
| 62 |
+
model="llama-3.3-70b-versatile",
|
| 63 |
+
temperature=0,
|
| 64 |
+
groq_api_key=os.getenv("GROQ_API")
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# --- NODE 1: decide_retrieval ---
|
| 68 |
+
def decide_retrieval(state: AgentState) -> dict:
|
| 69 |
+
query = state["query"]
|
| 70 |
+
|
| 71 |
+
prompt = f"""You are a routing assistant for a university advisory system.
|
| 72 |
+
|
| 73 |
+
Your job: Decide if the following query requires searching the university knowledge base
|
| 74 |
+
(course catalogs, academic policies, faculty directory) OR if it can be answered directly
|
| 75 |
+
from general knowledge or is a greeting/chitchat.
|
| 76 |
+
|
| 77 |
+
Query: "{query}"
|
| 78 |
+
|
| 79 |
+
Rules:
|
| 80 |
+
- If the query is a greeting, introduction, or social message β NO retrieval needed
|
| 81 |
+
- If the query asks about general concepts (e.g., "What does GPA stand for?") β NO retrieval needed
|
| 82 |
+
- If the query is about a specific course, prerequisite, credit hours, schedule β YES retrieval needed
|
| 83 |
+
- If the query is about university policies (grading, fees, attendance, withdrawal) β YES retrieval needed
|
| 84 |
+
- If the query is about a specific faculty member β YES retrieval needed
|
| 85 |
+
|
| 86 |
+
Respond in this EXACT JSON format (no other text):
|
| 87 |
+
{{
|
| 88 |
+
"needs_retrieval": true or false,
|
| 89 |
+
"reasoning": "one sentence explanation"
|
| 90 |
+
}}
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
response = llm.invoke(prompt)
|
| 94 |
+
data = safe_json_parse(response.content)
|
| 95 |
+
if data is None:
|
| 96 |
+
data = {"needs_retrieval": True, "reasoning": "Parsing error, defaulting to retrieval"}
|
| 97 |
+
|
| 98 |
+
checkpoint_data = {
|
| 99 |
+
"checkpoint": 1,
|
| 100 |
+
"label": "RETRIEVAL DECISION",
|
| 101 |
+
"status": "pass" if data["needs_retrieval"] else "skip",
|
| 102 |
+
"detail": f"Needs Retrieval: {data['needs_retrieval']}",
|
| 103 |
+
"reasoning": data["reasoning"]
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return {
|
| 107 |
+
"needs_retrieval": data["needs_retrieval"],
|
| 108 |
+
"retrieval_reasoning": data["reasoning"],
|
| 109 |
+
"agent_path": "decide_retrieval",
|
| 110 |
+
"trace_steps": [checkpoint_data]
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
# --- NODE 2: retrieve ---
|
| 114 |
+
def retrieve(state: AgentState) -> dict:
|
| 115 |
+
query = state["query"]
|
| 116 |
+
print(f"\n[RETRIEVAL] Searching knowledge base for: '{query}'")
|
| 117 |
+
|
| 118 |
+
docs = retrieve_documents.invoke({"query": query})
|
| 119 |
+
|
| 120 |
+
print(f" Retrieved {len(docs)} document chunks.")
|
| 121 |
+
for i, doc in enumerate(docs):
|
| 122 |
+
print(f" Doc {i+1}: {doc['content'][:100]}... [Source: {doc['metadata'].get('source_file','unknown')}]")
|
| 123 |
+
|
| 124 |
+
return {
|
| 125 |
+
"retrieved_docs": docs,
|
| 126 |
+
"agent_path": state["agent_path"] + " β retrieve"
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
import asyncio
|
| 130 |
+
|
| 131 |
+
# --- NODE 3: grade_documents ---
|
| 132 |
+
async def grade_documents(state: AgentState) -> dict:
|
| 133 |
+
query = state["query"]
|
| 134 |
+
docs = state.get("retrieved_docs", [])
|
| 135 |
+
relevant_docs = []
|
| 136 |
+
|
| 137 |
+
if not docs:
|
| 138 |
+
return {
|
| 139 |
+
"relevant_docs": [],
|
| 140 |
+
"use_web_search": True,
|
| 141 |
+
"agent_path": state["agent_path"] + " β grade_documents (no docs)",
|
| 142 |
+
"trace_steps": state["trace_steps"]
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
print(f"\n[CHECKPOINT 2 - RELEVANCE GRADING] Parallel batch grading {len(docs)} documents in 2 batches...")
|
| 146 |
+
|
| 147 |
+
# Split into batches of 5 (for a total of 10 docs)
|
| 148 |
+
batch1 = docs[:5]
|
| 149 |
+
batch2 = docs[5:10]
|
| 150 |
+
|
| 151 |
+
async def grade_batch(batch, batch_num):
|
| 152 |
+
if not batch:
|
| 153 |
+
return []
|
| 154 |
+
|
| 155 |
+
print(f" β Batch {batch_num}: Processing {len(batch)} docs via Groq...")
|
| 156 |
+
batch_text = ""
|
| 157 |
+
for i, doc in enumerate(batch):
|
| 158 |
+
batch_text += f"\n--- DOCUMENT {i+1} ---\n{doc['content']}\n"
|
| 159 |
+
|
| 160 |
+
prompt = f"""You are a relevance grader.
|
| 161 |
+
Query: "{query}"
|
| 162 |
+
Docs: {batch_text}
|
| 163 |
+
|
| 164 |
+
CRITICAL: If a document contains information about a course, teacher, or university policy that MIGHT be related to the query, mark it as relevant. Be generous.
|
| 165 |
+
|
| 166 |
+
Respond ONLY in JSON:
|
| 167 |
+
{{ "results": [ {{ "index": 1, "relevant": true }}, ... ] }}
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
response = await llm.ainvoke(prompt)
|
| 172 |
+
parsed = safe_json_parse(response.content)
|
| 173 |
+
|
| 174 |
+
batch_relevant = []
|
| 175 |
+
if parsed and "results" in parsed:
|
| 176 |
+
for res in parsed["results"]:
|
| 177 |
+
idx = res.get("index", 0) - 1
|
| 178 |
+
if 0 <= idx < len(batch) and res.get("relevant"):
|
| 179 |
+
batch_relevant.append(batch[idx])
|
| 180 |
+
|
| 181 |
+
print(f" β Batch {batch_num} Finished.")
|
| 182 |
+
return batch_relevant
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print(f" Γ Batch {batch_num} Error: {e}")
|
| 185 |
+
return []
|
| 186 |
+
|
| 187 |
+
# Run 2 batches in parallel
|
| 188 |
+
results = await asyncio.gather(
|
| 189 |
+
grade_batch(batch1, 1),
|
| 190 |
+
grade_batch(batch2, 2)
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# Flatten results
|
| 194 |
+
for batch_result in results:
|
| 195 |
+
relevant_docs.extend(batch_result)
|
| 196 |
+
|
| 197 |
+
relevant_count = len(relevant_docs)
|
| 198 |
+
total_count = len(docs)
|
| 199 |
+
|
| 200 |
+
checkpoint_data = {
|
| 201 |
+
"checkpoint": 2,
|
| 202 |
+
"label": "RELEVANCE GRADING",
|
| 203 |
+
"status": "pass" if relevant_count > 0 else "fail",
|
| 204 |
+
"detail": f"{relevant_count}/{total_count} documents relevant (Parallel Batched)",
|
| 205 |
+
"docs_graded": total_count,
|
| 206 |
+
"docs_relevant": relevant_count,
|
| 207 |
+
"web_search_triggered": relevant_count == 0
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
return {
|
| 211 |
+
"relevant_docs": relevant_docs,
|
| 212 |
+
"use_web_search": relevant_count == 0,
|
| 213 |
+
"agent_path": state["agent_path"] + " β grade_documents",
|
| 214 |
+
"trace_steps": state["trace_steps"] + [checkpoint_data]
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
# --- NODE 4: web_search_node ---
|
| 218 |
+
def web_search_node(state: AgentState) -> dict:
|
| 219 |
+
query = state["query"]
|
| 220 |
+
print(f"\n[WEB SEARCH FALLBACK] Searching web for: '{query}'")
|
| 221 |
+
|
| 222 |
+
results = web_search.invoke({"query": query})
|
| 223 |
+
|
| 224 |
+
print(f" Found {len(results)} web results.")
|
| 225 |
+
|
| 226 |
+
return {
|
| 227 |
+
"web_results": results,
|
| 228 |
+
"agent_path": state["agent_path"] + " β web_search_node"
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
# --- NODE 5: prepare_context ---
|
| 232 |
+
def prepare_context(state: AgentState) -> dict:
|
| 233 |
+
if state.get("use_web_search") and state.get("web_results"):
|
| 234 |
+
context_parts = []
|
| 235 |
+
for result in state["web_results"]:
|
| 236 |
+
context_parts.append(f"[Web Source: {result.get('url', 'unknown')}]\n{result.get('content', '')}")
|
| 237 |
+
context = "\n\n---\n\n".join(context_parts)
|
| 238 |
+
print(f"\n[CONTEXT PREPARED] Using web search results as context.")
|
| 239 |
+
elif state.get("relevant_docs"):
|
| 240 |
+
context_parts = []
|
| 241 |
+
for doc in state["relevant_docs"]:
|
| 242 |
+
meta = doc["metadata"]
|
| 243 |
+
source_label = f"[Source: {meta.get('source_file','?')} | Dept: {meta.get('department','?')}]"
|
| 244 |
+
context_parts.append(f"{source_label}\n{doc['content']}")
|
| 245 |
+
context = "\n\n---\n\n".join(context_parts)
|
| 246 |
+
print(f"\n[CONTEXT PREPARED] Using {len(state['relevant_docs'])} relevant documents.")
|
| 247 |
+
else:
|
| 248 |
+
context = ""
|
| 249 |
+
print(f"\n[CONTEXT PREPARED] No context available.")
|
| 250 |
+
|
| 251 |
+
return {
|
| 252 |
+
"generation_context": context,
|
| 253 |
+
"agent_path": state["agent_path"] + " β prepare_context"
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
# --- NODE 6: generate ---
|
| 257 |
+
def generate(state: AgentState) -> dict:
|
| 258 |
+
query = state["query"]
|
| 259 |
+
context = state.get("generation_context", "")
|
| 260 |
+
retry_count = state.get("retry_count", 0)
|
| 261 |
+
|
| 262 |
+
print(f"\n[GENERATION] Generating response (attempt {retry_count + 1})...")
|
| 263 |
+
if context:
|
| 264 |
+
print(f" β Using {len(context)} characters of retrieved context.")
|
| 265 |
+
else:
|
| 266 |
+
print(f" β No context available, using general knowledge.")
|
| 267 |
+
|
| 268 |
+
memory_context = state.get("memory_context", "")
|
| 269 |
+
|
| 270 |
+
if context:
|
| 271 |
+
prompt = f"""You are a helpful university course advisory assistant for XYZ National University.
|
| 272 |
+
|
| 273 |
+
Previous conversation context:
|
| 274 |
+
{memory_context}
|
| 275 |
+
|
| 276 |
+
CRITICAL INSTRUCTIONS:
|
| 277 |
+
1. EXHAUSTIVE LISTING: You must list EACH AND EVERY course found in the context. Search the context thoroughly for every mention of a course code or title.
|
| 278 |
+
2. CLEAN FORMATTING: Do NOT use asterisks (*) for bullets or bolding. Use numbered headers (e.g., 1., 2., 3.) for main courses and plain text for details.
|
| 279 |
+
3. STRUCTURE: For each course, provide: Name, Credits, Prerequisites, Instructor, and a brief Description.
|
| 280 |
+
4. ACCURACY: If a course mentions 'CS-501' but the context says 'Advanced Machine Learning', use the title from the context.
|
| 281 |
+
|
| 282 |
+
Context:
|
| 283 |
+
{context}
|
| 284 |
+
|
| 285 |
+
Student Question: {query}
|
| 286 |
+
|
| 287 |
+
Answer clearly and professionally:
|
| 288 |
+
"""
|
| 289 |
+
else:
|
| 290 |
+
prompt = f"""You are a helpful university course advisory assistant.
|
| 291 |
+
|
| 292 |
+
Previous conversation context:
|
| 293 |
+
{memory_context}
|
| 294 |
+
|
| 295 |
+
Answer the following question from your general knowledge.
|
| 296 |
+
|
| 297 |
+
Question: {query}
|
| 298 |
+
|
| 299 |
+
Provide a clear and helpful answer:
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
response = llm.invoke(prompt)
|
| 303 |
+
generated = response.content
|
| 304 |
+
|
| 305 |
+
print(f" Generated response: {generated[:200]}...")
|
| 306 |
+
|
| 307 |
+
return {
|
| 308 |
+
"response": generated,
|
| 309 |
+
"agent_path": state["agent_path"] + " β generate"
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
# --- NODE 7: check_hallucination ---
|
| 313 |
+
def check_hallucination(state: AgentState) -> dict:
|
| 314 |
+
response = state["response"]
|
| 315 |
+
context = state.get("generation_context", "")
|
| 316 |
+
retry_count = state.get("retry_count", 0)
|
| 317 |
+
use_web = state.get("use_web_search", False)
|
| 318 |
+
|
| 319 |
+
print(f"\n[CHECKPOINT 3 - HALLUCINATION CHECK] (attempt {retry_count + 1})")
|
| 320 |
+
|
| 321 |
+
# Force hallucination detection for demonstration purposes
|
| 322 |
+
if HALLUCINATION_TEST_MODE and retry_count == 0:
|
| 323 |
+
print(" [TEST MODE] Forcing hallucination detection for demonstration.")
|
| 324 |
+
return {
|
| 325 |
+
"hallucination_detected": True,
|
| 326 |
+
"retry_count": 1,
|
| 327 |
+
"hallucination_explanation": "TEST MODE: Forced hallucination for demonstration."
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
# Skip check if no retrieval was used (direct general knowledge answer)
|
| 331 |
+
if not state.get("needs_retrieval", True):
|
| 332 |
+
print(" β No retrieval was used. Skipping hallucination check.")
|
| 333 |
+
return {
|
| 334 |
+
"hallucination_detected": False,
|
| 335 |
+
"final_answer": response
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
# Skip check if no context is available
|
| 339 |
+
if not context:
|
| 340 |
+
print(" β No context available. Skipping hallucination check.")
|
| 341 |
+
return {
|
| 342 |
+
"hallucination_detected": False,
|
| 343 |
+
"final_answer": response
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
# Use DIFFERENT strictness levels based on source type
|
| 347 |
+
if use_web:
|
| 348 |
+
strictness_instruction = """
|
| 349 |
+
This response was generated from WEB SEARCH RESULTS, not official documents.
|
| 350 |
+
Apply LENIENT checking:
|
| 351 |
+
- Only flag it if the response invents completely fabricated facts with
|
| 352 |
+
no basis whatsoever in the web results.
|
| 353 |
+
- Reasonable inference, summarization, and synthesis from web results is ALLOWED.
|
| 354 |
+
- Minor extrapolation (e.g., inferring a rank number from ranking context) is OK.
|
| 355 |
+
- Do NOT flag responses that reasonably summarize or paraphrase web results.
|
| 356 |
+
- Only return hallucination_detected=true if the response contains a specific
|
| 357 |
+
claim that directly contradicts the web results."""
|
| 358 |
+
else:
|
| 359 |
+
strictness_instruction = """
|
| 360 |
+
This response was generated from OFFICIAL UNIVERSITY DOCUMENTS.
|
| 361 |
+
Apply STRICT checking:
|
| 362 |
+
- Flag any specific fact (number, name, policy, fee, grade, course code)
|
| 363 |
+
that is NOT present in the context.
|
| 364 |
+
- Do NOT flag the university name "XYZ National University" as a hallucination.
|
| 365 |
+
- Do NOT flag polite conversational phrases or general framing.
|
| 366 |
+
- Do NOT flag minor rephrasing or summarization of document content."""
|
| 367 |
+
|
| 368 |
+
prompt = f"""You are a hallucination detector for a university advisory AI.
|
| 369 |
+
|
| 370 |
+
{strictness_instruction}
|
| 371 |
+
|
| 372 |
+
Context:
|
| 373 |
+
---
|
| 374 |
+
{context}
|
| 375 |
+
---
|
| 376 |
+
|
| 377 |
+
Generated Response:
|
| 378 |
+
---
|
| 379 |
+
{response}
|
| 380 |
+
---
|
| 381 |
+
|
| 382 |
+
Respond in this EXACT JSON format (no other text):
|
| 383 |
+
{{
|
| 384 |
+
"hallucination_detected": true or false,
|
| 385 |
+
"explanation": "specific invented fact found, or 'none' if clean"
|
| 386 |
+
}}
|
| 387 |
+
"""
|
| 388 |
+
|
| 389 |
+
result = llm.invoke(prompt)
|
| 390 |
+
data = safe_json_parse(result.content)
|
| 391 |
+
if data is None:
|
| 392 |
+
data = {"hallucination_detected": False, "explanation": "Parsing error, assuming clean"}
|
| 393 |
+
|
| 394 |
+
print(f" Hallucination detected: {data['hallucination_detected']}")
|
| 395 |
+
print(f" Explanation: {data['explanation']}")
|
| 396 |
+
|
| 397 |
+
checkpoint_data = {
|
| 398 |
+
"checkpoint": 3,
|
| 399 |
+
"label": "HALLUCINATION CHECK",
|
| 400 |
+
"status": "fail" if data["hallucination_detected"] else "pass",
|
| 401 |
+
"detail": data["explanation"],
|
| 402 |
+
"retries_used": retry_count
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
if data["hallucination_detected"]:
|
| 406 |
+
new_retry_count = retry_count + 1
|
| 407 |
+
print(f" β Hallucination found! Retry count: {new_retry_count}")
|
| 408 |
+
|
| 409 |
+
return {
|
| 410 |
+
"hallucination_detected": True,
|
| 411 |
+
"retry_count": new_retry_count,
|
| 412 |
+
"agent_path": state["agent_path"] + " β check_hallucination",
|
| 413 |
+
"trace_steps": state["trace_steps"] + [checkpoint_data]
|
| 414 |
+
}
|
| 415 |
+
else:
|
| 416 |
+
return {
|
| 417 |
+
"hallucination_detected": False,
|
| 418 |
+
"final_answer": response,
|
| 419 |
+
"agent_path": state["agent_path"] + " β check_hallucination β END",
|
| 420 |
+
"trace_steps": state["trace_steps"] + [checkpoint_data]
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
# --- NODE 8: direct_answer ---
|
| 424 |
+
def direct_answer(state: AgentState) -> dict:
|
| 425 |
+
query = state["query"]
|
| 426 |
+
memory_context = state.get("memory_context", "")
|
| 427 |
+
|
| 428 |
+
print(f"\n[DIRECT ANSWER] No retrieval needed. Answering from general knowledge.")
|
| 429 |
+
|
| 430 |
+
prompt = f"""You are a friendly and helpful university course advisory assistant.
|
| 431 |
+
|
| 432 |
+
Previous conversation context:
|
| 433 |
+
{memory_context}
|
| 434 |
+
|
| 435 |
+
Answer the following message naturally and helpfully.
|
| 436 |
+
|
| 437 |
+
Message: {query}
|
| 438 |
+
"""
|
| 439 |
+
|
| 440 |
+
response = llm.invoke(prompt)
|
| 441 |
+
print(f" Response: {response.content[:200]}...")
|
| 442 |
+
|
| 443 |
+
return {
|
| 444 |
+
"final_answer": response.content,
|
| 445 |
+
"needs_retrieval": False,
|
| 446 |
+
"agent_path": state["agent_path"] + " β direct_answer β END"
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
# --- NODE 9: max_retries_exceeded ---
|
| 450 |
+
MAX_RETRIES = 2
|
| 451 |
+
|
| 452 |
+
def max_retries_exceeded(state: AgentState) -> dict:
|
| 453 |
+
print(f"\n[MAX RETRIES EXCEEDED] Could not generate a verified response after {MAX_RETRIES} attempts.")
|
| 454 |
+
disclaimer = (
|
| 455 |
+
"I was unable to generate a verified response for your query. "
|
| 456 |
+
"The information I found may be incomplete or inconsistent. "
|
| 457 |
+
"Please contact the university directly or visit the official university portal for accurate information."
|
| 458 |
+
)
|
| 459 |
+
return {
|
| 460 |
+
"final_answer": disclaimer,
|
| 461 |
+
"agent_path": state["agent_path"] + " β max_retries_exceeded β END"
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
# --- CONDITIONAL EDGE FUNCTIONS ---
|
| 465 |
+
def route_after_retrieval_decision(state: AgentState) -> str:
|
| 466 |
+
if state["needs_retrieval"]:
|
| 467 |
+
return "retrieve"
|
| 468 |
+
else:
|
| 469 |
+
return "direct_answer"
|
| 470 |
+
|
| 471 |
+
def route_after_grading(state: AgentState) -> str:
|
| 472 |
+
if state["use_web_search"]:
|
| 473 |
+
return "web_search_node"
|
| 474 |
+
else:
|
| 475 |
+
return "prepare_context"
|
| 476 |
+
|
| 477 |
+
def route_after_hallucination_check(state: AgentState) -> str:
|
| 478 |
+
if not state["hallucination_detected"]:
|
| 479 |
+
return END
|
| 480 |
+
elif state.get("retry_count", 0) >= MAX_RETRIES:
|
| 481 |
+
return "max_retries_exceeded"
|
| 482 |
+
else:
|
| 483 |
+
return "generate"
|
| 484 |
+
|
| 485 |
+
# --- BUILD THE STATEGRAPH ---
|
| 486 |
+
def build_graph():
|
| 487 |
+
graph = StateGraph(AgentState)
|
| 488 |
+
|
| 489 |
+
graph.add_node("decide_retrieval", decide_retrieval)
|
| 490 |
+
graph.add_node("retrieve", retrieve)
|
| 491 |
+
graph.add_node("grade_documents", grade_documents)
|
| 492 |
+
graph.add_node("web_search_node", web_search_node)
|
| 493 |
+
graph.add_node("prepare_context", prepare_context)
|
| 494 |
+
graph.add_node("generate", generate)
|
| 495 |
+
graph.add_node("check_hallucination", check_hallucination)
|
| 496 |
+
graph.add_node("direct_answer", direct_answer)
|
| 497 |
+
graph.add_node("max_retries_exceeded", max_retries_exceeded)
|
| 498 |
+
|
| 499 |
+
graph.set_entry_point("decide_retrieval")
|
| 500 |
+
|
| 501 |
+
graph.add_edge("retrieve", "grade_documents")
|
| 502 |
+
graph.add_edge("web_search_node", "prepare_context")
|
| 503 |
+
graph.add_edge("prepare_context", "generate")
|
| 504 |
+
graph.add_edge("generate", "check_hallucination")
|
| 505 |
+
graph.add_edge("max_retries_exceeded", END)
|
| 506 |
+
graph.add_edge("direct_answer", END)
|
| 507 |
+
|
| 508 |
+
graph.add_conditional_edges(
|
| 509 |
+
"decide_retrieval",
|
| 510 |
+
route_after_retrieval_decision,
|
| 511 |
+
{
|
| 512 |
+
"retrieve": "retrieve",
|
| 513 |
+
"direct_answer": "direct_answer"
|
| 514 |
+
}
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
graph.add_conditional_edges(
|
| 518 |
+
"grade_documents",
|
| 519 |
+
route_after_grading,
|
| 520 |
+
{
|
| 521 |
+
"web_search_node": "web_search_node",
|
| 522 |
+
"prepare_context": "prepare_context"
|
| 523 |
+
}
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
graph.add_conditional_edges(
|
| 527 |
+
"check_hallucination",
|
| 528 |
+
route_after_hallucination_check,
|
| 529 |
+
{
|
| 530 |
+
END: END,
|
| 531 |
+
"max_retries_exceeded": "max_retries_exceeded",
|
| 532 |
+
"generate": "generate"
|
| 533 |
+
}
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
return graph.compile()
|
| 537 |
+
|
| 538 |
+
app = build_graph()
|
ingest.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 2 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 3 |
+
from langchain_community.vectorstores import Chroma
|
| 4 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
# --- EMBEDDING MODEL ---
|
| 8 |
+
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
| 9 |
+
|
| 10 |
+
# --- PDF LIST ---
|
| 11 |
+
PDF_SOURCES = [
|
| 12 |
+
{
|
| 13 |
+
"path": "data/CS_Department_Catalog.pdf",
|
| 14 |
+
"department": "Computer Science",
|
| 15 |
+
"doc_type": "course_catalog",
|
| 16 |
+
"course_level": "undergraduate_graduate"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"path": "data/EE_Department_Catalog.pdf",
|
| 20 |
+
"department": "Electrical Engineering",
|
| 21 |
+
"doc_type": "course_catalog",
|
| 22 |
+
"course_level": "undergraduate"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"path": "data/BBA_Department_Catalog.pdf",
|
| 26 |
+
"department": "Business Administration",
|
| 27 |
+
"doc_type": "course_catalog",
|
| 28 |
+
"course_level": "undergraduate"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"path": "data/University_Academic_Policies.pdf",
|
| 32 |
+
"department": "University",
|
| 33 |
+
"doc_type": "academic_policy",
|
| 34 |
+
"course_level": "all"
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"path": "data/Faculty_Directory.pdf",
|
| 38 |
+
"department": "All",
|
| 39 |
+
"doc_type": "faculty_directory",
|
| 40 |
+
"course_level": "all"
|
| 41 |
+
}
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
import shutil
|
| 45 |
+
|
| 46 |
+
def create_vector_store():
|
| 47 |
+
"""
|
| 48 |
+
Creates a pure-Python vector store (pickle) to bypass ChromaDB/Python 3.14 crashes.
|
| 49 |
+
"""
|
| 50 |
+
print("Initializing embeddings...")
|
| 51 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 52 |
+
embeddings_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 53 |
+
|
| 54 |
+
all_chunks = []
|
| 55 |
+
# Increase size and overlap to keep headers with content
|
| 56 |
+
splitter = RecursiveCharacterTextSplitter(
|
| 57 |
+
chunk_size=1500,
|
| 58 |
+
chunk_overlap=400,
|
| 59 |
+
separators=["\n\n", "\n", ".", " ", ""]
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
for source in PDF_SOURCES:
|
| 63 |
+
if not os.path.exists(source["path"]):
|
| 64 |
+
print(f"Warning: File not found {source['path']}")
|
| 65 |
+
continue
|
| 66 |
+
|
| 67 |
+
print(f"Processing: {source['path']}")
|
| 68 |
+
loader = PyPDFLoader(source["path"])
|
| 69 |
+
pages = loader.load()
|
| 70 |
+
|
| 71 |
+
# Inject metadata directly into text to ensure context is never lost
|
| 72 |
+
for doc in pages:
|
| 73 |
+
prefix = f"[DEPARTMENT: {source['department']}] [DOC: {source['doc_type']}]\n"
|
| 74 |
+
doc.page_content = prefix + doc.page_content
|
| 75 |
+
|
| 76 |
+
doc.metadata["department"] = source["department"]
|
| 77 |
+
doc.metadata["doc_type"] = source["doc_type"]
|
| 78 |
+
doc.metadata["course_level"] = source["course_level"]
|
| 79 |
+
doc.metadata["source_file"] = source["path"]
|
| 80 |
+
|
| 81 |
+
chunks = splitter.split_documents(pages)
|
| 82 |
+
all_chunks.extend(chunks)
|
| 83 |
+
|
| 84 |
+
print(f"Embedding {len(all_chunks)} chunks (Pure Python)...")
|
| 85 |
+
texts = [chunk.page_content for chunk in all_chunks]
|
| 86 |
+
embeddings = embeddings_model.embed_documents(texts)
|
| 87 |
+
|
| 88 |
+
db_path = "vector_store_fallback.pkl"
|
| 89 |
+
import pickle
|
| 90 |
+
with open(db_path, "wb") as f:
|
| 91 |
+
pickle.dump({"chunks": all_chunks, "embeddings": embeddings}, f)
|
| 92 |
+
|
| 93 |
+
print(f"Saved {len(all_chunks)} chunks to {db_path}")
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
create_vector_store()
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
langchain==0.2.16
|
| 2 |
+
langchain-community==0.2.16
|
| 3 |
+
langchain-google-genai==1.0.10
|
| 4 |
+
langgraph==0.2.28
|
| 5 |
+
chromadb==0.5.5
|
| 6 |
+
pypdf==4.3.1
|
| 7 |
+
tiktoken>=0.7.0
|
| 8 |
+
pydantic==2.8.2
|
| 9 |
+
tavily-python==0.3.3
|
| 10 |
+
python-dotenv==1.0.1
|
| 11 |
+
sentence-transformers==3.0.1
|
| 12 |
+
langchain-huggingface==0.0.3
|
| 13 |
+
fastapi==0.111.0
|
| 14 |
+
uvicorn==0.30.1
|
scratch/check_retrieval.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ingest import get_retriever
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
def check_query(query):
|
| 5 |
+
retriever = get_retriever()
|
| 6 |
+
docs = retriever.invoke(query)
|
| 7 |
+
print(f"\n--- Search results for: '{query}' ---\n")
|
| 8 |
+
for i, doc in enumerate(docs):
|
| 9 |
+
print(f"Result {i+1} (Source: {doc.metadata.get('source_file')}):")
|
| 10 |
+
print(f"{doc.page_content}\n")
|
| 11 |
+
print("-" * 50)
|
| 12 |
+
|
| 13 |
+
if __name__ == "__main__":
|
| 14 |
+
teacher_query = "Farhan Qureshi"
|
| 15 |
+
if len(sys.argv) > 1:
|
| 16 |
+
teacher_query = sys.argv[1]
|
| 17 |
+
check_query(teacher_query)
|
self_rag_agent.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
import uvicorn
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Load environment variables
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
from graph import app as langgraph_app
|
| 13 |
+
|
| 14 |
+
class MemoryItem(BaseModel):
|
| 15 |
+
role: str
|
| 16 |
+
content: str
|
| 17 |
+
|
| 18 |
+
class ChatRequest(BaseModel):
|
| 19 |
+
query: str
|
| 20 |
+
window_memory: List[MemoryItem] = []
|
| 21 |
+
|
| 22 |
+
class CheckpointData(BaseModel):
|
| 23 |
+
checkpoint: int
|
| 24 |
+
label: str
|
| 25 |
+
status: str
|
| 26 |
+
detail: str
|
| 27 |
+
reasoning: Optional[str] = None
|
| 28 |
+
docs_graded: Optional[int] = None
|
| 29 |
+
docs_relevant: Optional[int] = None
|
| 30 |
+
web_search_triggered: Optional[bool] = False
|
| 31 |
+
retries_used: Optional[int] = 0
|
| 32 |
+
|
| 33 |
+
class ChatResponse(BaseModel):
|
| 34 |
+
answer: str
|
| 35 |
+
path: str
|
| 36 |
+
trace: List[CheckpointData]
|
| 37 |
+
used_web_search: bool
|
| 38 |
+
retry_count: int
|
| 39 |
+
|
| 40 |
+
app_api = FastAPI(title="XYZ University Advisory Agent API")
|
| 41 |
+
|
| 42 |
+
app_api.add_middleware(
|
| 43 |
+
CORSMiddleware,
|
| 44 |
+
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
| 45 |
+
allow_credentials=True,
|
| 46 |
+
allow_methods=["*"],
|
| 47 |
+
allow_headers=["*"],
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
@app_api.post("/chat", response_model=ChatResponse)
|
| 51 |
+
async def chat_endpoint(request: ChatRequest):
|
| 52 |
+
try:
|
| 53 |
+
# Build initial state including window memory as context
|
| 54 |
+
memory_context = ""
|
| 55 |
+
if request.window_memory:
|
| 56 |
+
memory_context = "\n\nRecent conversation context:\n"
|
| 57 |
+
for item in request.window_memory:
|
| 58 |
+
role_label = "Student" if item.role == "user" else "Advisor"
|
| 59 |
+
memory_context += f"{role_label}: {item.content}\n"
|
| 60 |
+
|
| 61 |
+
initial_state = {
|
| 62 |
+
"query": request.query,
|
| 63 |
+
"memory_context": memory_context,
|
| 64 |
+
"needs_retrieval": False,
|
| 65 |
+
"retrieval_reasoning": "",
|
| 66 |
+
"retrieved_docs": [],
|
| 67 |
+
"relevant_docs": [],
|
| 68 |
+
"use_web_search": False,
|
| 69 |
+
"web_results": [],
|
| 70 |
+
"generation_context": "",
|
| 71 |
+
"response": "",
|
| 72 |
+
"hallucination_detected": False,
|
| 73 |
+
"retry_count": 0,
|
| 74 |
+
"final_answer": "",
|
| 75 |
+
"trace_steps": [],
|
| 76 |
+
"agent_path": ""
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
final_state = await langgraph_app.ainvoke(initial_state)
|
| 80 |
+
|
| 81 |
+
# Build trace from final_state
|
| 82 |
+
trace = final_state.get("trace_steps", [])
|
| 83 |
+
|
| 84 |
+
return ChatResponse(
|
| 85 |
+
answer=final_state.get("final_answer", "No answer generated."),
|
| 86 |
+
path=final_state.get("agent_path", "unknown"),
|
| 87 |
+
trace=trace,
|
| 88 |
+
used_web_search=final_state.get("use_web_search", False),
|
| 89 |
+
retry_count=final_state.get("retry_count", 0)
|
| 90 |
+
)
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f"Error in chat endpoint: {e}")
|
| 93 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 94 |
+
|
| 95 |
+
@app_api.get("/health")
|
| 96 |
+
async def health():
|
| 97 |
+
return {"status": "online", "agent": "XYZ University Advisory Agent"}
|
| 98 |
+
|
| 99 |
+
# --- SILENCE EXTERNAL NOISE ---
|
| 100 |
+
# These endpoints are added to stop 404 logs from external dashboards (VPTQ)
|
| 101 |
+
@app_api.get("/api/pipeline/status")
|
| 102 |
+
@app_api.get("/api/stats/pipeline-status")
|
| 103 |
+
async def pipeline_status():
|
| 104 |
+
return {"status": "idle", "message": "University Advisor Active"}
|
| 105 |
+
|
| 106 |
+
@app_api.post("/api/stats/run-pipeline")
|
| 107 |
+
async def run_pipeline():
|
| 108 |
+
return {"status": "skipped", "message": "Pipeline logic not applicable to RAG Agent"}
|
| 109 |
+
|
| 110 |
+
if __name__ == "__main__":
|
| 111 |
+
uvicorn.run("self_rag_agent:app_api", host="0.0.0.0", port=8000, reload=True)
|
tools.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.tools import tool
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
import numpy as np
|
| 7 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 8 |
+
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
# Initialize embeddings once
|
| 12 |
+
embeddings_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 13 |
+
|
| 14 |
+
# --- TOOL 1: retrieve_documents ---
|
| 15 |
+
|
| 16 |
+
class RetrieveInput(BaseModel):
|
| 17 |
+
query: str = Field(description="The user's question to search for in the university knowledge base")
|
| 18 |
+
|
| 19 |
+
@tool(args_schema=RetrieveInput)
|
| 20 |
+
def retrieve_documents(query: str, k: int = 15) -> list[dict]:
|
| 21 |
+
"""
|
| 22 |
+
Retrieves relevant document chunks using Python 3.14 safe similarity search.
|
| 23 |
+
"""
|
| 24 |
+
import pickle
|
| 25 |
+
db_path = "vector_store_fallback.pkl"
|
| 26 |
+
if not os.path.exists(db_path):
|
| 27 |
+
print(f"Error: {db_path} not found.")
|
| 28 |
+
return []
|
| 29 |
+
|
| 30 |
+
with open(db_path, "rb") as f:
|
| 31 |
+
data = pickle.load(f)
|
| 32 |
+
|
| 33 |
+
chunks = data["chunks"]
|
| 34 |
+
chunk_embeddings = np.array(data["embeddings"])
|
| 35 |
+
|
| 36 |
+
# Embed the query
|
| 37 |
+
query_embedding = np.array(embeddings_model.embed_query(query))
|
| 38 |
+
|
| 39 |
+
# Calculate cosine similarity: (A . B) / (||A|| * ||B||)
|
| 40 |
+
dot_products = np.dot(chunk_embeddings, query_embedding)
|
| 41 |
+
norms_chunks = np.linalg.norm(chunk_embeddings, axis=1)
|
| 42 |
+
norm_query = np.linalg.norm(query_embedding)
|
| 43 |
+
similarities = dot_products / (norms_chunks * norm_query)
|
| 44 |
+
|
| 45 |
+
# Get top k indices
|
| 46 |
+
top_indices = np.argsort(similarities)[::-1][:k]
|
| 47 |
+
|
| 48 |
+
results = []
|
| 49 |
+
for idx in top_indices:
|
| 50 |
+
results.append({
|
| 51 |
+
"content": chunks[idx].page_content,
|
| 52 |
+
"metadata": chunks[idx].metadata
|
| 53 |
+
})
|
| 54 |
+
return results
|
| 55 |
+
|
| 56 |
+
# --- TOOL 2: web_search ---
|
| 57 |
+
|
| 58 |
+
class WebSearchInput(BaseModel):
|
| 59 |
+
query: str = Field(description="Search query for finding information on the web when the knowledge base does not have the answer")
|
| 60 |
+
|
| 61 |
+
@tool(args_schema=WebSearchInput)
|
| 62 |
+
def web_search(query: str) -> list[dict]:
|
| 63 |
+
"""
|
| 64 |
+
Performs a web search using Tavily to find information not available in the
|
| 65 |
+
university's internal knowledge base. Use this as a FALLBACK tool only when
|
| 66 |
+
all retrieved documents from the knowledge base were graded as irrelevant.
|
| 67 |
+
Returns a list of web search results with title, content, and URL.
|
| 68 |
+
"""
|
| 69 |
+
search = TavilySearchResults(max_results=3, tavily_api_key=os.getenv("TAVILY_API_KEY"))
|
| 70 |
+
results = search.invoke(query)
|
| 71 |
+
return results
|
university-advisor-ui/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>XYZ University Advisory Agent</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body class="bg-brand-bg font-body text-brand-text antialiased">
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
university-advisor-ui/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
university-advisor-ui/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "university-advisor-ui",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^18.3.1",
|
| 14 |
+
"react-dom": "^18.3.1",
|
| 15 |
+
"lucide-react": "^0.383.0"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@types/react": "^18.3.3",
|
| 19 |
+
"@types/react-dom": "^18.3.0",
|
| 20 |
+
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
| 21 |
+
"@typescript-eslint/parser": "^7.13.1",
|
| 22 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 23 |
+
"autoprefixer": "^10.4.19",
|
| 24 |
+
"eslint": "^8.57.0",
|
| 25 |
+
"eslint-plugin-react-hooks": "^4.6.2",
|
| 26 |
+
"eslint-plugin-react-refresh": "^0.4.7",
|
| 27 |
+
"postcss": "^8.4.38",
|
| 28 |
+
"tailwindcss": "^3.4.4",
|
| 29 |
+
"typescript": "^5.2.2",
|
| 30 |
+
"vite": "^5.3.1"
|
| 31 |
+
}
|
| 32 |
+
}
|
university-advisor-ui/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
university-advisor-ui/src/App.tsx
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import Header from './components/Header';
|
| 3 |
+
import Sidebar from './components/Sidebar';
|
| 4 |
+
import MessageBubble from './components/MessageBubble';
|
| 5 |
+
import TracePanel from './components/TracePanel';
|
| 6 |
+
import InputBar from './components/InputBar';
|
| 7 |
+
import { Message, TraceStep, WindowMemoryItem } from './types';
|
| 8 |
+
import { sendMessage, ChatResponse } from './services/api';
|
| 9 |
+
|
| 10 |
+
const App: React.FC = () => {
|
| 11 |
+
const [messages, setMessages] = useState<Message[]>([
|
| 12 |
+
{
|
| 13 |
+
id: 'welcome',
|
| 14 |
+
role: 'assistant',
|
| 15 |
+
content: "Welcome to the XYZ University advisory portal. How can I help you today?",
|
| 16 |
+
timestamp: new Date(),
|
| 17 |
+
}
|
| 18 |
+
]);
|
| 19 |
+
const [windowMemory, setWindowMemory] = useState<WindowMemoryItem[]>([]);
|
| 20 |
+
const [inputText, setInputText] = useState("");
|
| 21 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 22 |
+
const [sidebarOpen, setSidebarOpen] = useState(true);
|
| 23 |
+
const [traceOpen, setTraceOpen] = useState(false);
|
| 24 |
+
const [activeTrace, setActiveTrace] = useState<{ steps: TraceStep[], path: string }>({ steps: [], path: "" });
|
| 25 |
+
|
| 26 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 27 |
+
|
| 28 |
+
useEffect(() => {
|
| 29 |
+
if (scrollRef.current) {
|
| 30 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 31 |
+
}
|
| 32 |
+
}, [messages]);
|
| 33 |
+
|
| 34 |
+
const updateWindowMemory = (userMsg: string, botMsg: string) => {
|
| 35 |
+
setWindowMemory(prev => {
|
| 36 |
+
const newItems: WindowMemoryItem[] = [
|
| 37 |
+
{ role: 'user', content: userMsg },
|
| 38 |
+
{ role: 'assistant', content: botMsg }
|
| 39 |
+
];
|
| 40 |
+
const combined = [...prev, ...newItems];
|
| 41 |
+
return combined.slice(-4);
|
| 42 |
+
});
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const handleSend = async (forcedQuery?: string) => {
|
| 46 |
+
const query = forcedQuery || inputText;
|
| 47 |
+
if (!query.trim() || isLoading) return;
|
| 48 |
+
|
| 49 |
+
const userMessage: Message = {
|
| 50 |
+
id: Date.now().toString(),
|
| 51 |
+
role: 'user',
|
| 52 |
+
content: query,
|
| 53 |
+
timestamp: new Date(),
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
setMessages(prev => [...prev, userMessage]);
|
| 57 |
+
setInputText("");
|
| 58 |
+
setIsLoading(true);
|
| 59 |
+
|
| 60 |
+
const botPlaceholderId = (Date.now() + 1).toString();
|
| 61 |
+
const botLoadingMessage: Message = {
|
| 62 |
+
id: botPlaceholderId,
|
| 63 |
+
role: 'assistant',
|
| 64 |
+
content: "",
|
| 65 |
+
timestamp: new Date(),
|
| 66 |
+
isLoading: true
|
| 67 |
+
};
|
| 68 |
+
setMessages(prev => [...prev, botLoadingMessage]);
|
| 69 |
+
|
| 70 |
+
try {
|
| 71 |
+
const response: ChatResponse = await sendMessage({
|
| 72 |
+
query,
|
| 73 |
+
window_memory: windowMemory
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const traceSteps: TraceStep[] = response.trace.map((t, i) => ({
|
| 77 |
+
...t,
|
| 78 |
+
id: `t-${Date.now()}-${i}`,
|
| 79 |
+
timestamp: new Date()
|
| 80 |
+
}));
|
| 81 |
+
|
| 82 |
+
const botMessage: Message = {
|
| 83 |
+
id: botPlaceholderId,
|
| 84 |
+
role: 'assistant',
|
| 85 |
+
content: response.answer,
|
| 86 |
+
timestamp: new Date(),
|
| 87 |
+
trace: traceSteps,
|
| 88 |
+
path: response.path
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
setMessages(prev => prev.map(m => m.id === botPlaceholderId ? botMessage : m));
|
| 92 |
+
setActiveTrace({ steps: traceSteps, path: response.path });
|
| 93 |
+
updateWindowMemory(query, response.answer);
|
| 94 |
+
|
| 95 |
+
} catch (error) {
|
| 96 |
+
const errorMessage: Message = {
|
| 97 |
+
id: botPlaceholderId,
|
| 98 |
+
role: 'assistant',
|
| 99 |
+
content: "System error: Failed to connect to university records.",
|
| 100 |
+
timestamp: new Date(),
|
| 101 |
+
};
|
| 102 |
+
setMessages(prev => prev.map(m => m.id === botPlaceholderId ? errorMessage : m));
|
| 103 |
+
} finally {
|
| 104 |
+
setIsLoading(false);
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
return (
|
| 109 |
+
<div className="flex flex-col h-screen bg-[#070B14] text-slate-200 overflow-hidden font-body">
|
| 110 |
+
<Header onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} />
|
| 111 |
+
|
| 112 |
+
<div className="flex-1 flex overflow-hidden">
|
| 113 |
+
{/* Sidebar */}
|
| 114 |
+
<Sidebar
|
| 115 |
+
isOpen={sidebarOpen}
|
| 116 |
+
onQuickTopic={handleSend}
|
| 117 |
+
windowMemory={windowMemory}
|
| 118 |
+
/>
|
| 119 |
+
|
| 120 |
+
{/* Main Chat Area */}
|
| 121 |
+
<main className="flex-1 flex flex-col relative overflow-hidden">
|
| 122 |
+
{/* Messages Feed */}
|
| 123 |
+
<div
|
| 124 |
+
ref={scrollRef}
|
| 125 |
+
className="flex-1 overflow-y-auto px-4 py-6 scroll-smooth"
|
| 126 |
+
>
|
| 127 |
+
<div className="max-w-4xl mx-auto w-full">
|
| 128 |
+
{messages.map((msg) => (
|
| 129 |
+
<MessageBubble
|
| 130 |
+
key={msg.id}
|
| 131 |
+
message={msg}
|
| 132 |
+
onTraceClick={() => setTraceOpen(true)}
|
| 133 |
+
/>
|
| 134 |
+
))}
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
{/* Trace Panel */}
|
| 139 |
+
<TracePanel
|
| 140 |
+
isOpen={traceOpen}
|
| 141 |
+
onToggle={() => setTraceOpen(!traceOpen)}
|
| 142 |
+
steps={activeTrace.steps}
|
| 143 |
+
path={activeTrace.path}
|
| 144 |
+
/>
|
| 145 |
+
|
| 146 |
+
{/* Input Area */}
|
| 147 |
+
<div className="w-full">
|
| 148 |
+
<InputBar
|
| 149 |
+
value={inputText}
|
| 150 |
+
onChange={setInputText}
|
| 151 |
+
onSend={() => handleSend()}
|
| 152 |
+
isLoading={isLoading}
|
| 153 |
+
/>
|
| 154 |
+
</div>
|
| 155 |
+
</main>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
);
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
export default App;
|
university-advisor-ui/src/components/Header.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { GraduationCap, Menu } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
interface HeaderProps {
|
| 5 |
+
onToggleSidebar: () => void;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
| 9 |
+
return (
|
| 10 |
+
<header className="h-14 bg-[#040811] border-b border-slate-800 flex items-center justify-between px-4 shrink-0 z-50">
|
| 11 |
+
<div className="flex items-center gap-3">
|
| 12 |
+
<button
|
| 13 |
+
onClick={onToggleSidebar}
|
| 14 |
+
className="text-slate-500 hover:text-slate-300 transition-colors p-1"
|
| 15 |
+
>
|
| 16 |
+
<Menu size={18} />
|
| 17 |
+
</button>
|
| 18 |
+
<div className="flex items-center gap-2">
|
| 19 |
+
<GraduationCap size={18} className="text-amber-400" />
|
| 20 |
+
<span className="text-sm font-semibold text-slate-200"
|
| 21 |
+
style={{fontFamily: 'Playfair Display, serif'}}>
|
| 22 |
+
XYZ National University
|
| 23 |
+
</span>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div className="flex items-center gap-2.5 bg-slate-900/50 px-3 py-1.5 rounded-full border border-slate-800">
|
| 28 |
+
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
|
| 29 |
+
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-widest font-bold">Agent Online</span>
|
| 30 |
+
</div>
|
| 31 |
+
</header>
|
| 32 |
+
);
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
export default Header;
|
university-advisor-ui/src/components/InputBar.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect } from 'react';
|
| 2 |
+
import { Send, Loader2 } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
interface InputBarProps {
|
| 5 |
+
value: string;
|
| 6 |
+
onChange: (value: string) => void;
|
| 7 |
+
onSend: () => void;
|
| 8 |
+
isLoading: boolean;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const InputBar: React.FC<InputBarProps> = ({ value, onChange, onSend, isLoading }) => {
|
| 12 |
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
if (textareaRef.current) {
|
| 16 |
+
textareaRef.current.style.height = 'auto';
|
| 17 |
+
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`;
|
| 18 |
+
}
|
| 19 |
+
}, [value]);
|
| 20 |
+
|
| 21 |
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
| 22 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 23 |
+
e.preventDefault();
|
| 24 |
+
onSend();
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<div className="p-3 border-t border-slate-800 bg-[#040811] shrink-0">
|
| 30 |
+
<div className="flex gap-2 items-end max-w-4xl mx-auto w-full">
|
| 31 |
+
<textarea
|
| 32 |
+
ref={textareaRef}
|
| 33 |
+
value={value}
|
| 34 |
+
onChange={e => onChange(e.target.value)}
|
| 35 |
+
onKeyDown={handleKeyDown}
|
| 36 |
+
placeholder="Ask about courses, faculty, policies..."
|
| 37 |
+
rows={2}
|
| 38 |
+
disabled={isLoading}
|
| 39 |
+
className="flex-1 bg-slate-900 border border-slate-800 rounded-xl px-4 py-3 text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-600 resize-none font-body transition-all disabled:opacity-50"
|
| 40 |
+
/>
|
| 41 |
+
<button
|
| 42 |
+
onClick={onSend}
|
| 43 |
+
disabled={isLoading || !value.trim()}
|
| 44 |
+
className="w-12 h-12 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-800 disabled:text-slate-600 text-white rounded-xl flex items-center justify-center transition-all shrink-0 shadow-lg shadow-blue-900/10 hover:shadow-blue-900/30 active:scale-95"
|
| 45 |
+
>
|
| 46 |
+
{isLoading ? (
|
| 47 |
+
<Loader2 size={18} className="animate-spin" />
|
| 48 |
+
) : (
|
| 49 |
+
<Send size={18} />
|
| 50 |
+
)}
|
| 51 |
+
</button>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
);
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
export default InputBar;
|
university-advisor-ui/src/components/MessageBubble.tsx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { GraduationCap, Loader2 } from 'lucide-react';
|
| 3 |
+
import { Message } from '../types';
|
| 4 |
+
|
| 5 |
+
interface MessageBubbleProps {
|
| 6 |
+
message: Message;
|
| 7 |
+
onTraceClick: (checkpoint: number) => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, onTraceClick }) => {
|
| 11 |
+
const isUser = message.role === "user";
|
| 12 |
+
const formatTime = (date: Date) => {
|
| 13 |
+
return new Date(date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
if (message.isLoading) {
|
| 17 |
+
return (
|
| 18 |
+
<div className="flex justify-start mb-6 animate-fade-slide-up">
|
| 19 |
+
<div className="max-w-[75%]">
|
| 20 |
+
<div className="flex items-center gap-1.5 mb-1.5 pl-1">
|
| 21 |
+
<GraduationCap size={12} className="text-amber-400" />
|
| 22 |
+
<span className="text-xs text-amber-400/80 font-semibold tracking-wide">XYZ Advisor</span>
|
| 23 |
+
</div>
|
| 24 |
+
<div className="bg-slate-900 border border-slate-800 rounded-2xl rounded-tl-sm px-4 py-3 border-l-2 border-l-blue-600 flex items-center gap-3">
|
| 25 |
+
<div className="flex gap-1">
|
| 26 |
+
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-typing-bounce" style={{ animationDelay: '0ms' }}></div>
|
| 27 |
+
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-typing-bounce" style={{ animationDelay: '150ms' }}></div>
|
| 28 |
+
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-typing-bounce" style={{ animationDelay: '300ms' }}></div>
|
| 29 |
+
</div>
|
| 30 |
+
<span className="text-xs text-slate-500 font-mono italic">Analyzing...</span>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (isUser) {
|
| 38 |
+
return (
|
| 39 |
+
<div className="flex justify-end mb-4 animate-fade-slide-up">
|
| 40 |
+
<div className="max-w-[70%] bg-blue-900/40 border border-blue-800/50 rounded-2xl rounded-tr-sm px-4 py-3">
|
| 41 |
+
<p className="text-sm text-slate-200 leading-relaxed font-body">
|
| 42 |
+
{message.content}
|
| 43 |
+
</p>
|
| 44 |
+
<p className="text-xs text-slate-600 mt-1.5 text-right font-mono">
|
| 45 |
+
{formatTime(message.timestamp)}
|
| 46 |
+
</p>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
const trace = message.trace;
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<div className="flex justify-start mb-6 animate-fade-slide-up">
|
| 56 |
+
<div className="max-w-[75%]">
|
| 57 |
+
{/* Advisor label */}
|
| 58 |
+
<div className="flex items-center gap-1.5 mb-1.5 pl-1">
|
| 59 |
+
<GraduationCap size={12} className="text-amber-400" />
|
| 60 |
+
<span className="text-xs text-amber-400/80 font-semibold tracking-wide">
|
| 61 |
+
XYZ Advisor
|
| 62 |
+
</span>
|
| 63 |
+
<span className="text-xs text-slate-700 font-mono ml-4">
|
| 64 |
+
{formatTime(message.timestamp)}
|
| 65 |
+
</span>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{/* Message content */}
|
| 69 |
+
<div className="bg-slate-900 border border-slate-800 rounded-2xl rounded-tl-sm px-4 py-3 border-l-2 border-l-blue-600 shadow-sm">
|
| 70 |
+
<p className="text-sm text-slate-200 leading-relaxed whitespace-pre-wrap font-body">
|
| 71 |
+
{message.content}
|
| 72 |
+
</p>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
{/* Minimal checkpoint row */}
|
| 76 |
+
{trace && (
|
| 77 |
+
<div className="flex items-center gap-2 mt-2 pl-1">
|
| 78 |
+
{/* Checkpoint 1 - Retrieval */}
|
| 79 |
+
<div className="flex items-center gap-1.5">
|
| 80 |
+
<div className={`w-2 h-2 rounded-full ${
|
| 81 |
+
trace?.find(t => t.checkpoint === 1)?.status === 'pass'
|
| 82 |
+
? 'bg-emerald-500' : 'bg-slate-600'
|
| 83 |
+
}`} />
|
| 84 |
+
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-tight">
|
| 85 |
+
{trace?.find(t => t.checkpoint === 1)?.status === 'skip'
|
| 86 |
+
? 'direct' : 'retrieved'}
|
| 87 |
+
</span>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<span className="text-slate-800 text-[10px]">β’</span>
|
| 91 |
+
|
| 92 |
+
{/* Checkpoint 2 - Grading */}
|
| 93 |
+
<div className="flex items-center gap-1.5">
|
| 94 |
+
<div className={`w-2 h-2 rounded-full ${
|
| 95 |
+
trace?.find(t => t.checkpoint === 2)?.web_search_triggered
|
| 96 |
+
? 'bg-amber-500'
|
| 97 |
+
: trace?.find(t => t.checkpoint === 2)?.status === 'pass'
|
| 98 |
+
? 'bg-emerald-500' : 'bg-slate-600'
|
| 99 |
+
}`} />
|
| 100 |
+
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-tight">
|
| 101 |
+
{trace?.find(t => t.checkpoint === 2)?.web_search_triggered
|
| 102 |
+
? 'web'
|
| 103 |
+
: `${trace?.find(t => t.checkpoint === 2)?.docs_relevant ?? 0}/${trace?.find(t => t.checkpoint === 2)?.docs_graded ?? 0} docs`}
|
| 104 |
+
</span>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<span className="text-slate-800 text-[10px]">β’</span>
|
| 108 |
+
|
| 109 |
+
{/* Checkpoint 3 - Hallucination */}
|
| 110 |
+
<div className="flex items-center gap-1.5">
|
| 111 |
+
<div className={`w-2 h-2 rounded-full ${
|
| 112 |
+
(trace?.find(t => t.checkpoint === 3)?.retries_used ?? 0) > 0
|
| 113 |
+
? 'bg-amber-500' : 'bg-emerald-500'
|
| 114 |
+
}`} />
|
| 115 |
+
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-tight">verified</span>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
{/* Expand trace button */}
|
| 119 |
+
<button
|
| 120 |
+
onClick={() => onTraceClick(0)} // Pass 0 or any dummy, trace panel handles active message
|
| 121 |
+
className="ml-auto text-[10px] text-slate-600 hover:text-blue-400 font-mono transition-colors uppercase tracking-wider"
|
| 122 |
+
>
|
| 123 |
+
view trace βΊ
|
| 124 |
+
</button>
|
| 125 |
+
</div>
|
| 126 |
+
)}
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
);
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
export default MessageBubble;
|
university-advisor-ui/src/components/Sidebar.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import {
|
| 3 |
+
BookOpen,
|
| 4 |
+
Users,
|
| 5 |
+
ShieldCheck,
|
| 6 |
+
CreditCard,
|
| 7 |
+
Calendar,
|
| 8 |
+
GraduationCap
|
| 9 |
+
} from 'lucide-react';
|
| 10 |
+
import { WindowMemoryItem } from '../types';
|
| 11 |
+
|
| 12 |
+
interface SidebarProps {
|
| 13 |
+
onQuickTopic: (topic: string) => void;
|
| 14 |
+
windowMemory: WindowMemoryItem[];
|
| 15 |
+
isOpen: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const Sidebar: React.FC<SidebarProps> = ({ onQuickTopic, windowMemory, isOpen }) => {
|
| 19 |
+
if (!isOpen) return null;
|
| 20 |
+
|
| 21 |
+
const quickTopics = [
|
| 22 |
+
{ label: "Prerequisites", icon: <BookOpen size={14} />, query: "What are the prerequisites for Advanced Algorithms?" },
|
| 23 |
+
{ label: "Faculty", icon: <Users size={14} />, query: "Who are the professors in the Computer Science department?" },
|
| 24 |
+
{ label: "Policies", icon: <ShieldCheck size={14} />, query: "What is the policy for academic probation?" },
|
| 25 |
+
{ label: "Fee Structure", icon: <CreditCard size={14} />, query: "What are the tuition fees for the BSCS program?" },
|
| 26 |
+
{ label: "Schedule", icon: <Calendar size={14} />, query: "When does the Fall 2025 semester start?" },
|
| 27 |
+
{ label: "Requirements", icon: <GraduationCap size={14} />, query: "What are the total credit hours required for graduation?" },
|
| 28 |
+
];
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<aside className="w-64 h-full bg-[#0D1117] border-r border-slate-800 flex flex-col relative shrink-0 transition-all duration-150">
|
| 32 |
+
<div className="p-4">
|
| 33 |
+
{/* Logo area */}
|
| 34 |
+
<div className="mb-6">
|
| 35 |
+
<div className="flex items-center gap-2 mb-1">
|
| 36 |
+
<GraduationCap size={18} className="text-amber-400" />
|
| 37 |
+
<span className="text-sm font-semibold text-slate-200"
|
| 38 |
+
style={{fontFamily: 'Playfair Display, serif'}}>
|
| 39 |
+
XYZ Advisory
|
| 40 |
+
</span>
|
| 41 |
+
</div>
|
| 42 |
+
<p className="text-xs text-slate-600 pl-6 font-body">
|
| 43 |
+
Self-RAG Course Agent
|
| 44 |
+
</p>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
{/* Divider */}
|
| 48 |
+
<div className="border-t border-slate-800 mb-4" />
|
| 49 |
+
|
| 50 |
+
{/* Quick Topics */}
|
| 51 |
+
<p className="text-[10px] text-slate-600 uppercase tracking-[0.2em] mb-3 font-mono">
|
| 52 |
+
Quick Topics
|
| 53 |
+
</p>
|
| 54 |
+
<div className="flex flex-col gap-1">
|
| 55 |
+
{quickTopics.map(topic => (
|
| 56 |
+
<button
|
| 57 |
+
key={topic.label}
|
| 58 |
+
onClick={() => onQuickTopic(topic.query)}
|
| 59 |
+
className="flex items-center gap-3 text-left px-3 py-2 rounded-lg text-xs text-slate-400
|
| 60 |
+
hover:bg-slate-800 hover:text-slate-200
|
| 61 |
+
transition-colors duration-150 font-body"
|
| 62 |
+
>
|
| 63 |
+
<span className="opacity-50">{topic.icon}</span>
|
| 64 |
+
{topic.label}
|
| 65 |
+
</button>
|
| 66 |
+
))}
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
{/* Context indicator at bottom */}
|
| 70 |
+
<div className="absolute bottom-4 left-4 right-4">
|
| 71 |
+
<div className="bg-slate-900 rounded-lg p-3 border border-slate-800 shadow-sm">
|
| 72 |
+
<p className="text-[10px] font-mono text-slate-600 mb-1 uppercase tracking-wider">
|
| 73 |
+
π¬ Memory window
|
| 74 |
+
</p>
|
| 75 |
+
<p className="text-[11px] text-slate-500 font-body">
|
| 76 |
+
{windowMemory.length > 0
|
| 77 |
+
? `${windowMemory.length / 2} exchange(s) in context`
|
| 78 |
+
: 'No context yet'}
|
| 79 |
+
</p>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
</aside>
|
| 84 |
+
);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
export default Sidebar;
|
university-advisor-ui/src/components/TracePanel.tsx
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Terminal,
|
| 4 |
+
ChevronUp,
|
| 5 |
+
ChevronDown,
|
| 6 |
+
CheckCircle2,
|
| 7 |
+
XCircle,
|
| 8 |
+
FastForward,
|
| 9 |
+
Loader2,
|
| 10 |
+
Globe,
|
| 11 |
+
RefreshCcw,
|
| 12 |
+
ArrowRight
|
| 13 |
+
} from 'lucide-react';
|
| 14 |
+
import { TraceStep } from '../types';
|
| 15 |
+
|
| 16 |
+
interface TracePanelProps {
|
| 17 |
+
isOpen: boolean;
|
| 18 |
+
onToggle: () => void;
|
| 19 |
+
steps: TraceStep[];
|
| 20 |
+
path: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const TracePanel: React.FC<TracePanelProps> = ({ isOpen, onToggle, steps, path }) => {
|
| 24 |
+
const panelRef = useRef<HTMLDivElement>(null);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
if (isOpen && panelRef.current) {
|
| 28 |
+
panelRef.current.scrollTop = panelRef.current.scrollHeight;
|
| 29 |
+
}
|
| 30 |
+
}, [isOpen, steps]);
|
| 31 |
+
|
| 32 |
+
return (
|
| 33 |
+
<div className={`w-full bg-[#050810] border-t border-brand-border transition-all duration-300 flex flex-col ${isOpen ? 'h-52' : 'h-10'}`}>
|
| 34 |
+
{/* Header Toggle */}
|
| 35 |
+
<button
|
| 36 |
+
onClick={onToggle}
|
| 37 |
+
className="h-10 px-6 flex items-center justify-between hover:bg-brand-border/30 transition-colors shrink-0 group"
|
| 38 |
+
>
|
| 39 |
+
<div className="flex items-center gap-3">
|
| 40 |
+
<Terminal size={14} className="text-brand-muted group-hover:text-brand-accent transition-colors" />
|
| 41 |
+
<span className="text-[11px] font-mono font-bold text-brand-muted group-hover:text-brand-text uppercase tracking-widest">
|
| 42 |
+
Agent Execution Trace
|
| 43 |
+
</span>
|
| 44 |
+
{path && isOpen && (
|
| 45 |
+
<div className="hidden md:flex items-center gap-2 ml-4 px-3 py-0.5 bg-brand-accent/10 rounded-full border border-brand-accent/20">
|
| 46 |
+
<span className="text-[9px] font-mono text-brand-accent/80 font-bold uppercase">Flow Path:</span>
|
| 47 |
+
<span className="text-[9px] font-mono text-brand-muted tracking-tighter overflow-hidden whitespace-nowrap max-w-[400px]">
|
| 48 |
+
{path}
|
| 49 |
+
</span>
|
| 50 |
+
</div>
|
| 51 |
+
)}
|
| 52 |
+
</div>
|
| 53 |
+
<div className="flex items-center gap-4">
|
| 54 |
+
{steps.length > 0 && !isOpen && (
|
| 55 |
+
<span className="text-[10px] font-mono text-brand-success/60 font-bold uppercase animate-pulse">
|
| 56 |
+
{steps.length} Steps Logged
|
| 57 |
+
</span>
|
| 58 |
+
)}
|
| 59 |
+
{isOpen ? <ChevronDown size={14} className="text-brand-muted" /> : <ChevronUp size={14} className="text-brand-muted" />}
|
| 60 |
+
</div>
|
| 61 |
+
</button>
|
| 62 |
+
|
| 63 |
+
{/* Content Area */}
|
| 64 |
+
<div
|
| 65 |
+
ref={panelRef}
|
| 66 |
+
className={`flex-1 overflow-y-auto px-6 py-4 space-y-4 font-mono text-xs ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
| 67 |
+
>
|
| 68 |
+
{steps.length === 0 ? (
|
| 69 |
+
<div className="h-full flex items-center justify-center text-brand-muted/20 flex-col gap-2">
|
| 70 |
+
<Terminal size={24} strokeWidth={1} />
|
| 71 |
+
<span className="text-[10px] uppercase tracking-[0.2em]">Awaiting execution data...</span>
|
| 72 |
+
</div>
|
| 73 |
+
) : (
|
| 74 |
+
steps.map((step, idx) => {
|
| 75 |
+
const statusConfig = {
|
| 76 |
+
pass: { icon: <CheckCircle2 size={12} className="text-brand-success" />, color: 'text-brand-success' },
|
| 77 |
+
fail: { icon: <XCircle size={12} className="text-brand-danger" />, color: 'text-brand-danger' },
|
| 78 |
+
skip: { icon: <FastForward size={12} className="text-brand-muted" />, color: 'text-brand-muted' },
|
| 79 |
+
pending: { icon: <Loader2 size={12} className="text-brand-muted animate-spin" />, color: 'text-brand-muted' }
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const checkpointConfig = {
|
| 83 |
+
1: { label: 'C1', color: 'bg-brand-accent/20 text-brand-accent border-brand-accent/40' },
|
| 84 |
+
2: { label: 'C2', color: 'bg-brand-warning/20 text-brand-warning border-brand-warning/40' },
|
| 85 |
+
3: { label: 'C3', color: 'bg-brand-checkpoint/20 text-brand-checkpoint border-brand-checkpoint/40' }
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
const cp = checkpointConfig[step.checkpoint as 1|2|3];
|
| 89 |
+
const sc = statusConfig[step.status];
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div key={idx} className="space-y-1.5 animate-fade-slide-up" style={{ animationDelay: `${idx * 50}ms` }}>
|
| 93 |
+
<div className="flex items-center gap-3">
|
| 94 |
+
<span className="text-brand-muted/40 tabular-nums">[{new Date(step.timestamp).toLocaleTimeString([], { hour12: false })}]</span>
|
| 95 |
+
<span className={`text-[9px] px-1.5 py-0.5 rounded border font-black ${cp.color}`}>{cp.label}</span>
|
| 96 |
+
<div className="shrink-0">{sc.icon}</div>
|
| 97 |
+
<span className="text-brand-text font-bold uppercase tracking-tight">{step.label}:</span>
|
| 98 |
+
<span className="text-brand-muted truncate">{step.detail}</span>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
{step.reasoning && (
|
| 102 |
+
<div className="pl-24 flex gap-2">
|
| 103 |
+
<ArrowRight size={10} className="mt-1 text-brand-gold/40 shrink-0" />
|
| 104 |
+
<p className="text-brand-goldLight/70 italic leading-relaxed text-[11px] font-body">
|
| 105 |
+
{step.reasoning}
|
| 106 |
+
</p>
|
| 107 |
+
</div>
|
| 108 |
+
)}
|
| 109 |
+
|
| 110 |
+
{(step.web_search_triggered || step.retries_used! > 0) && (
|
| 111 |
+
<div className="pl-24 flex gap-2 pt-1">
|
| 112 |
+
{step.web_search_triggered && (
|
| 113 |
+
<span className="flex items-center gap-1.5 bg-brand-warning/10 text-brand-warning px-2 py-0.5 rounded border border-brand-warning/20 text-[9px] font-black uppercase">
|
| 114 |
+
<Globe size={10} />
|
| 115 |
+
Web Search Fallback
|
| 116 |
+
</span>
|
| 117 |
+
)}
|
| 118 |
+
{step.retries_used! > 0 && (
|
| 119 |
+
<span className="flex items-center gap-1.5 bg-brand-danger/10 text-brand-danger px-2 py-0.5 rounded border border-brand-danger/20 text-[9px] font-black uppercase">
|
| 120 |
+
<RefreshCcw size={10} />
|
| 121 |
+
Retry Count: {step.retries_used}
|
| 122 |
+
</span>
|
| 123 |
+
)}
|
| 124 |
+
</div>
|
| 125 |
+
)}
|
| 126 |
+
</div>
|
| 127 |
+
);
|
| 128 |
+
})
|
| 129 |
+
)}
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
);
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
export default TracePanel;
|
university-advisor-ui/src/index.css
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400..800&family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Azeret+Mono:ital,wght@0,100..900;1,100..900&display=swap');
|
| 2 |
+
|
| 3 |
+
@tailwind base;
|
| 4 |
+
@tailwind components;
|
| 5 |
+
@tailwind utilities;
|
| 6 |
+
|
| 7 |
+
@layer base {
|
| 8 |
+
body {
|
| 9 |
+
@apply bg-[#070B14] text-slate-200 font-body overflow-hidden selection:bg-blue-500/30;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
h1, h2, h3, h4, h5, h6 {
|
| 13 |
+
@apply font-display tracking-tight;
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
@keyframes fadeSlideUp {
|
| 18 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 19 |
+
to { opacity: 1; transform: translateY(0); }
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
@keyframes typingBounce {
|
| 23 |
+
0%, 80%, 100% { transform: translateY(0); }
|
| 24 |
+
40% { transform: translateY(-4px); }
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.animate-fade-slide-up {
|
| 28 |
+
animation: fadeSlideUp 0.2s ease-out forwards;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.animate-typing-bounce {
|
| 32 |
+
animation: typingBounce 1s infinite;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Transitions */
|
| 36 |
+
* {
|
| 37 |
+
@apply transition-colors duration-150 ease-out;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Custom Scrollbar */
|
| 41 |
+
::-webkit-scrollbar {
|
| 42 |
+
width: 4px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
::-webkit-scrollbar-track {
|
| 46 |
+
@apply bg-transparent;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
::-webkit-scrollbar-thumb {
|
| 50 |
+
@apply bg-slate-800 rounded-full hover:bg-slate-700;
|
| 51 |
+
}
|
university-advisor-ui/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
university-advisor-ui/src/services/api.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { WindowMemoryItem } from "../types"
|
| 2 |
+
|
| 3 |
+
const API_BASE = "http://localhost:8000"
|
| 4 |
+
const MOCK_MODE = false // Set to false when backend is running
|
| 5 |
+
|
| 6 |
+
export interface ChatRequest {
|
| 7 |
+
query: string
|
| 8 |
+
window_memory: WindowMemoryItem[]
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export interface CheckpointData {
|
| 12 |
+
checkpoint: number
|
| 13 |
+
label: string
|
| 14 |
+
status: "pass" | "fail" | "skip"
|
| 15 |
+
detail: string
|
| 16 |
+
reasoning?: string
|
| 17 |
+
docs_graded?: number
|
| 18 |
+
docs_relevant?: number
|
| 19 |
+
web_search_triggered?: boolean
|
| 20 |
+
retries_used?: number
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface ChatResponse {
|
| 24 |
+
answer: string
|
| 25 |
+
path: string
|
| 26 |
+
trace: CheckpointData[]
|
| 27 |
+
used_web_search: boolean
|
| 28 |
+
retry_count: number
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export const sendMessage = async (request: ChatRequest): Promise<ChatResponse> => {
|
| 32 |
+
if (MOCK_MODE) {
|
| 33 |
+
await new Promise(resolve => setTimeout(resolve, 1500))
|
| 34 |
+
return {
|
| 35 |
+
answer: "Based on the university catalog, CS-301 Advanced Algorithms requires CS-201 Data Structures as a prerequisite. The course is offered every Fall semester and carries 3 credit hours. The instructor is Dr. Ahmad Khan from the CS department.",
|
| 36 |
+
path: "decide_retrieval β retrieve β grade_documents β prepare_context β generate β check_hallucination β END",
|
| 37 |
+
trace: [
|
| 38 |
+
{ checkpoint: 1, label: "RETRIEVAL DECISION", status: "pass",
|
| 39 |
+
detail: "Needs Retrieval: True",
|
| 40 |
+
reasoning: "Query asks about specific course prerequisites requiring KB search." },
|
| 41 |
+
{ checkpoint: 2, label: "RELEVANCE GRADING", status: "pass",
|
| 42 |
+
detail: "3/4 documents relevant", docs_graded: 4, docs_relevant: 3,
|
| 43 |
+
web_search_triggered: false },
|
| 44 |
+
{ checkpoint: 3, label: "HALLUCINATION CHECK", status: "pass",
|
| 45 |
+
detail: "No hallucinations detected", retries_used: 0 }
|
| 46 |
+
],
|
| 47 |
+
used_web_search: false,
|
| 48 |
+
retry_count: 0
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
const response = await fetch(`${API_BASE}/chat`, {
|
| 53 |
+
method: "POST",
|
| 54 |
+
headers: { "Content-Type": "application/json" },
|
| 55 |
+
body: JSON.stringify(request)
|
| 56 |
+
})
|
| 57 |
+
|
| 58 |
+
if (!response.ok) {
|
| 59 |
+
throw new Error(`API error: ${response.status}`)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return response.json()
|
| 63 |
+
}
|
university-advisor-ui/src/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type MessageRole = "user" | "assistant" | "system"
|
| 2 |
+
|
| 3 |
+
export type CheckpointStatus = "pass" | "fail" | "skip" | "pending"
|
| 4 |
+
|
| 5 |
+
export interface TraceStep {
|
| 6 |
+
id: string
|
| 7 |
+
checkpoint: number // 1, 2, or 3
|
| 8 |
+
label: string // e.g. "RETRIEVAL DECISION"
|
| 9 |
+
status: CheckpointStatus
|
| 10 |
+
detail: string // e.g. "Needs Retrieval: True"
|
| 11 |
+
reasoning?: string // The reasoning text from the agent
|
| 12 |
+
docs_graded?: number // How many docs were graded (checkpoint 2)
|
| 13 |
+
docs_relevant?: number // How many were relevant (checkpoint 2)
|
| 14 |
+
web_search_triggered?: boolean
|
| 15 |
+
retries_used?: number
|
| 16 |
+
timestamp: Date
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface Message {
|
| 20 |
+
id: string
|
| 21 |
+
role: MessageRole
|
| 22 |
+
content: string
|
| 23 |
+
timestamp: Date
|
| 24 |
+
trace?: TraceStep[] // Only bot messages have traces
|
| 25 |
+
isLoading?: boolean // True while streaming
|
| 26 |
+
path?: string // e.g. "retrieve β grade β generate"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface WindowMemoryItem {
|
| 30 |
+
role: MessageRole
|
| 31 |
+
content: string
|
| 32 |
+
}
|
university-advisor-ui/tailwind.config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Config } from 'tailwindcss'
|
| 2 |
+
|
| 3 |
+
const config: Config = {
|
| 4 |
+
content: [
|
| 5 |
+
"./index.html",
|
| 6 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 7 |
+
],
|
| 8 |
+
theme: {
|
| 9 |
+
extend: {
|
| 10 |
+
colors: {
|
| 11 |
+
brand: {
|
| 12 |
+
bg: "#0A0E1A", // Deep navy β main background
|
| 13 |
+
surface: "#111827", // Slightly lighter β card/panel background
|
| 14 |
+
border: "#1E2D45", // Subtle border color
|
| 15 |
+
accent: "#2563EB", // Electric blue β primary action color
|
| 16 |
+
accentHov: "#1D4ED8", // Darker blue on hover
|
| 17 |
+
gold: "#F59E0B", // Amber gold β university branding accent
|
| 18 |
+
goldLight: "#FDE68A", // Light gold β for subtle highlights
|
| 19 |
+
text: "#F1F5F9", // Near white β primary text
|
| 20 |
+
muted: "#94A3B8", // Slate gray β secondary text, timestamps
|
| 21 |
+
user: "#1E3A5F", // Dark blue β user message bubble background
|
| 22 |
+
bot: "#0F1F35", // Darker navy β advisor message bubble background
|
| 23 |
+
success: "#10B981", // Emerald β checkpoint pass indicators
|
| 24 |
+
warning: "#F59E0B", // Amber β hallucination detected indicators
|
| 25 |
+
danger: "#EF4444", // Red β error states
|
| 26 |
+
checkpoint:"#7C3AED", // Purple β checkpoint badge backgrounds
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
fontFamily: {
|
| 30 |
+
display: ["Syne", "sans-serif"],
|
| 31 |
+
body: ["Bricolage Grotesque", "sans-serif"],
|
| 32 |
+
mono: ["Azeret Mono", "monospace"],
|
| 33 |
+
},
|
| 34 |
+
},
|
| 35 |
+
},
|
| 36 |
+
plugins: [],
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export default config
|
university-advisor-ui/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ESNext",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
| 6 |
+
"allowJs": false,
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"esModuleInterop": false,
|
| 9 |
+
"allowSyntheticDefaultImports": true,
|
| 10 |
+
"strict": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true,
|
| 12 |
+
"module": "ESNext",
|
| 13 |
+
"moduleResolution": "Node",
|
| 14 |
+
"resolveJsonModule": true,
|
| 15 |
+
"isolatedModules": true,
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx"
|
| 18 |
+
},
|
| 19 |
+
"include": ["src"],
|
| 20 |
+
"references": [{ "path": "./tsconfig.node.json" }]
|
| 21 |
+
}
|
university-advisor-ui/tsconfig.node.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"skipLibCheck": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "Node",
|
| 7 |
+
"allowSyntheticDefaultImports": true
|
| 8 |
+
},
|
| 9 |
+
"include": ["vite.config.ts"]
|
| 10 |
+
}
|
university-advisor-ui/vite.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vitejs.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
})
|