charan-ml commited on
Commit
466be16
·
1 Parent(s): 617777d

Deploy Space app files only

Browse files
Files changed (3) hide show
  1. README.md +168 -13
  2. app.py +557 -0
  3. requirements.txt +12 -0
README.md CHANGED
@@ -1,13 +1,168 @@
1
- ---
2
- title: KnowLedge
3
- emoji: 🏃
4
- colorFrom: yellow
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 6.12.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # KnowLedge
2
+
3
+ KnowLedge is a local-first learning verification platform built for the Gemma 4 for Good hackathon.
4
+
5
+ It helps students turn borrowed understanding into real mastery. The system tracks concepts that appear in pasted work, guides the learner through Socratic clearing sessions, verifies understanding with multimodal checks, and shares only privacy-preserving aggregates with instructors.
6
+
7
+ ## Why This Project Exists
8
+
9
+ Most AI learning tools optimize for speed. KnowLedge optimizes for retention and accountability.
10
+
11
+ The core idea is simple:
12
+
13
+ - If a student copies a concept without understanding it, the concept is logged as learning debt.
14
+ - If the student can explain it clearly in their own words, the concept is cleared.
15
+ - If the explanation still fails integrity checks, the concept stays under review.
16
+
17
+ The result is a system that uses AI to force better learning rather than passive answer collection.
18
+
19
+ ## What Judges Should Look For
20
+
21
+ - A polished desktop/web experience with Ledger, Progress, Reports, and Help views.
22
+ - Offline-first design using local SQLite, Ollama, and ChromaDB.
23
+ - Privacy-preserving instructor sync that shares only concept-level summaries.
24
+ - Integrity checks that combine session fingerprints, anti-spoof signals, and multimodal verification.
25
+ - A separate Expo mobile companion app with the same product model.
26
+
27
+ ## Product Overview
28
+
29
+ KnowLedge has three main workflows:
30
+
31
+ 1. Scout detects concepts in pasted material and logs them to the ledger.
32
+ 2. Sage runs a Socratic clearing session until the student can explain the concept themselves.
33
+ 3. Lens checks handwritten or visual work and helps flag concepts that still need review.
34
+
35
+ There is also a weekly instructor sync path that exports only anonymous, aggregate concept data.
36
+
37
+ ## Repository Structure
38
+
39
+ - `knowledge/` - FastAPI backend, orchestration, sync, integrity, RAG, and templates.
40
+ - `knowledge-mobile/` - Expo + React Native companion app.
41
+ - `cdt_vectorstore/` - Local ChromaDB persistence for curriculum context.
42
+ - `knowledge_dashboard.html` - Standalone dashboard prototype.
43
+ - `requirements.txt` - Python dependencies for the backend.
44
+
45
+ ## Backend Setup
46
+
47
+ ### 1. Create a Python environment
48
+
49
+ ```bash
50
+ python3 -m venv .venv
51
+ source .venv/bin/activate
52
+ ```
53
+
54
+ ### 2. Install backend dependencies
55
+
56
+ ```bash
57
+ pip install -r requirements.txt
58
+ ```
59
+
60
+ ### 3. Start Ollama
61
+
62
+ KnowLedge expects a local Ollama server.
63
+
64
+ ```bash
65
+ ollama serve
66
+ ```
67
+
68
+ If you are using the default Gemma tags, make sure the model names in `knowledge/config.py` exist in your local Ollama install.
69
+
70
+ ### 4. Run the app
71
+
72
+ ```bash
73
+ python -m uvicorn knowledge.main:app --host 127.0.0.1 --port 8000
74
+ ```
75
+
76
+ Open these views in a browser:
77
+
78
+ - `http://127.0.0.1:8000/ledger`
79
+ - `http://127.0.0.1:8000/progress`
80
+ - `http://127.0.0.1:8000/reports`
81
+ - `http://127.0.0.1:8000/help`
82
+
83
+ ### 5. Optional: load curriculum material into ChromaDB
84
+
85
+ ```bash
86
+ python -m knowledge.vectorize path/to/course_material.pdf
87
+ ```
88
+
89
+ This creates local retrieval context for Sage responses.
90
+
91
+ ## Mobile Companion
92
+
93
+ The mobile app lives in `knowledge-mobile/` and is separate from the backend.
94
+
95
+ ```bash
96
+ cd knowledge-mobile
97
+ npm install
98
+ npx expo start
99
+ ```
100
+
101
+ If you want a device build:
102
+
103
+ ```bash
104
+ npm run android
105
+ npm run ios
106
+ ```
107
+
108
+ The mobile app supports three runtime modes:
109
+
110
+ - `on_device_full`
111
+ - `on_device_scout`
112
+ - `server_only`
113
+
114
+ The native Gemma bridge is scaffolded for future Android and iOS implementation.
115
+
116
+ ## Live Demo For Kaggle
117
+
118
+ The best judge-facing option is a Hugging Face Space running the Streamlit demo in `app.py`.
119
+
120
+ Why this works well:
121
+
122
+ - It gives you a public HTTPS link with zero setup for judges.
123
+ - The Space can boot automatically when opened, so there is no manual server start step for reviewers.
124
+ - You can connect it to your hosted backend with a single environment variable, or let it fall back to demo data.
125
+
126
+ Recommended setup:
127
+
128
+ 1. Create a new Hugging Face Space.
129
+ 2. Select the **Gradio** SDK (industry standard for ML demo UIs).
130
+ 3. Point the Space to this repository's `app.py`.
131
+ 4. Add `gradio` to the dependencies file, which is already done.
132
+ 5. If you want live backend data, set a Space secret named `KNOWLEDGE_API_URL` to your deployed backend URL.
133
+ 6. Share the Space URL in your Kaggle submission as the live demo link.
134
+
135
+ If you want the Space to show live backend state, keep your FastAPI app deployed separately and set `KNOWLEDGE_API_URL` to that public endpoint. If you want a self-contained demo, the Space still works in fallback mode with sample data.
136
+
137
+ ## Configuration
138
+
139
+ Most backend settings live in `knowledge/config.py` and can also be overridden with environment variables.
140
+
141
+ Important values include:
142
+
143
+ - `OLLAMA_HOST`
144
+ - `SCOUT_MODEL`
145
+ - `SAGE_MODEL`
146
+ - `LENS_MODEL`
147
+ - `DB_PATH`
148
+ - `CHROMA_PATH`
149
+ - `SYNC_ON_WIFI_ONLY`
150
+
151
+ The Expo app has its own config in `knowledge-mobile/app.config.js` and `knowledge-mobile/app.json`.
152
+
153
+ ## Testing
154
+
155
+ Run the backend tests with:
156
+
157
+ ```bash
158
+ python -m pytest
159
+ ```
160
+
161
+ There are also lightweight smoke scripts in the repository root for quick local checks.
162
+
163
+ ## Notes for Reviewers
164
+
165
+ - The project is intentionally offline-first.
166
+ - Sync only shares aggregate concept data.
167
+ - The mobile native inference layer is scaffolded, not fully implemented.
168
+ - The main app is in the `knowledge/` package; the mobile app is an independent companion workspace.
app.py ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Any, Dict, List, Optional, Tuple
3
+
4
+ import gradio as gr
5
+ import httpx
6
+ import pandas as pd
7
+
8
+
9
+ # ─────────────────────────────────────────────────────────────────────────────
10
+ # Sample Data (Fallback when backend is unavailable)
11
+ # ─────────────────────────────────────────────────────────────────────────────
12
+
13
+ SAMPLE_LEDGER: List[Dict[str, Any]] = [
14
+ {"concept": "Recursion base case", "status": "on_loan", "confidence": 0.92, "source_text": "Borrowed from AI explanation"},
15
+ {"concept": "Binary search invariants", "status": "clear", "confidence": 0.88, "source_text": "Explained in own words"},
16
+ {"concept": "Gradient descent", "status": "persists", "confidence": 0.79, "source_text": "Needs another clearing session"},
17
+ {"concept": "RAG retrieval", "status": "on_loan", "confidence": 0.84, "source_text": "Added during prompt work"},
18
+ ]
19
+
20
+ SAMPLE_HEATMAP = [
21
+ {"concept": "Recursion base case", "count": 9},
22
+ {"concept": "Binary search invariants", "count": 4},
23
+ {"concept": "Gradient descent", "count": 7},
24
+ ]
25
+
26
+ SAMPLE_METRICS = {
27
+ "active": 2,
28
+ "cleared": 1,
29
+ "persists": 1,
30
+ "debt_score": 67,
31
+ "pending_sync": 0,
32
+ "spoof_attempts": 3,
33
+ }
34
+
35
+
36
+ # ─────────────────────────────────────────────────────────────────────────────
37
+ # Backend Integration
38
+ # ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ def backend_snapshot(base_url: str) -> Optional[Dict[str, Any]]:
41
+ """Fetch live data from the knowledge backend API."""
42
+ base = base_url.rstrip("/")
43
+ try:
44
+ with httpx.Client(timeout=6.0) as client:
45
+ state = client.get(f"{base}/api/state", timeout=6.0).json()
46
+ sync = client.get(f"{base}/api/sync/status", timeout=6.0).json()
47
+ integrity = client.get(f"{base}/api/integrity/report", timeout=6.0).json()
48
+ return {"state": state, "sync": sync, "integrity": integrity}
49
+ except Exception:
50
+ return None
51
+
52
+
53
+ def scout_demo(pasted_text: str) -> Tuple[str, str]:
54
+ """Simulate Scout tagging concepts from pasted text."""
55
+ if not pasted_text.strip():
56
+ return "", "Paste some text to see Scout extract concepts."
57
+
58
+ extracted_concepts = []
59
+ keywords = ["recursion", "binary search", "gradient descent", "rag", "algorithm", "model", "training", "inference"]
60
+ for keyword in keywords:
61
+ if keyword.lower() in pasted_text.lower():
62
+ extracted_concepts.append(keyword.capitalize())
63
+
64
+ if not extracted_concepts:
65
+ extracted_concepts = ["Learning (inferred from context)"]
66
+
67
+ result = f"**Scout detected {len(extracted_concepts)} concept(s):**\n\n"
68
+ for concept in extracted_concepts:
69
+ result += f"- {concept} (confidence: ~85%)\n"
70
+
71
+ status = f"✅ Tagged {len(extracted_concepts)} concept(s) → added to your ledger"
72
+ return result, status
73
+
74
+
75
+ def sage_demo(concept: str, user_response: str) -> str:
76
+ """Simulate Sage Socratic dialogue."""
77
+ if not concept.strip():
78
+ return "Enter a concept to start a clearing session."
79
+
80
+ if not user_response.strip():
81
+ return f"🦉 **Sage**: Let's talk about {concept}. Can you explain it in your own words?"
82
+
83
+ quality_score = len(user_response.split()) / 10
84
+ if quality_score < 2:
85
+ return f"🦉 **Sage**: That's a start. But can you go deeper? What makes {concept} special or different?"
86
+ elif quality_score < 4:
87
+ return f"🦉 **Sage**: Good effort! Now, why would someone use {concept} in practice? When does it matter?"
88
+ else:
89
+ return f"✅ **Sage**: Excellent! You've clearly understood {concept}. This concept is now **CLEARED** in your ledger."
90
+
91
+
92
+ def get_ledger_display() -> pd.DataFrame:
93
+ """Return the current ledger as a DataFrame for display."""
94
+ backend_url = os.environ.get("KNOWLEDGE_API_URL", "").strip()
95
+ snapshot = backend_snapshot(backend_url) if backend_url else None
96
+ ledger = snapshot["state"]["debts"] if snapshot else SAMPLE_LEDGER
97
+
98
+ df = pd.DataFrame(ledger)
99
+ if not df.empty:
100
+ df = df[["concept", "status", "confidence"]]
101
+ return df
102
+
103
+
104
+ def get_metrics() -> Tuple[int, int, int, int]:
105
+ """Return key metrics: active, cleared, persists, debt_score."""
106
+ backend_url = os.environ.get("KNOWLEDGE_API_URL", "").strip()
107
+ snapshot = backend_snapshot(backend_url) if backend_url else None
108
+ ledger = snapshot["state"]["debts"] if snapshot else SAMPLE_LEDGER
109
+
110
+ active = sum(1 for row in ledger if row.get("status") in {"on_loan", "persists"})
111
+ cleared = sum(1 for row in ledger if row.get("status") in {"clear", "owned"})
112
+ persists = sum(1 for row in ledger if row.get("status") == "persists")
113
+ debt_score = round(((active + persists) / max(len(ledger), 1)) * 100) if ledger else 0
114
+
115
+ return active, cleared, persists, debt_score
116
+
117
+
118
+ # ─────────────────────────────────────────────────────────────────────────────
119
+ # Gradio Interface
120
+ # ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ with gr.Blocks(
123
+ title="KnowLedge | Gemma 4 for Good",
124
+ theme=gr.themes.Soft(
125
+ primary_hue="slate",
126
+ secondary_hue="amber",
127
+ ),
128
+ ) as demo:
129
+
130
+ gr.Markdown(
131
+ """
132
+ # 🦉 KnowLedge
133
+
134
+ **A local-first learning verification system for the Gemma 4 for Good hackathon.**
135
+
136
+ KnowLedge turns pasted AI-assisted work into a guided mastery loop:
137
+ - **Scout** extracts concepts from text
138
+ - **Sage** clears them through Socratic dialogue
139
+ - **Lens** verifies understanding with integrity checks
140
+ - **Reports** share only anonymous aggregates with instructors
141
+ """
142
+ )
143
+
144
+ with gr.Row():
145
+ active, cleared, persists, debt_score = get_metrics()
146
+
147
+ with gr.Column(scale=1):
148
+ gr.Markdown(f"### 📊 Active\n\n**{active}** concepts on loan or persisting")
149
+ with gr.Column(scale=1):
150
+ gr.Markdown(f"### ✅ Cleared\n\n**{cleared}** owned concepts")
151
+ with gr.Column(scale=1):
152
+ gr.Markdown(f"### 🎯 Debt Score\n\n**{debt_score}%** (lower is better)")
153
+ with gr.Column(scale=1):
154
+ gr.Markdown(f"### 🛡️ Integrity\n\n**{SAMPLE_METRICS['spoof_attempts']}** spoof signals detected")
155
+
156
+ with gr.Tabs():
157
+
158
+ # ─────────────────────────────────────────────────────────────────────
159
+ # Tab 1: Scout Demo
160
+ # ─────────────────────────────────────────────────────────────────────
161
+ with gr.TabItem("🔍 Scout — Extract Concepts"):
162
+ gr.Markdown(
163
+ """
164
+ **Scout** automatically finds concepts inside pasted text. Try pasting an explanation or code snippet below.
165
+ """
166
+ )
167
+
168
+ with gr.Row():
169
+ with gr.Column(scale=2):
170
+ pasted_text = gr.Textbox(
171
+ label="Paste AI-assisted work or notes here",
172
+ placeholder="e.g., 'Recursion works by dividing a problem into smaller subproblems until reaching a base case...'",
173
+ lines=6,
174
+ )
175
+ with gr.Column(scale=1):
176
+ scout_btn = gr.Button("🚀 Run Scout", size="lg")
177
+
178
+ scout_output = gr.Markdown("Paste something to get started.")
179
+ scout_status = gr.Textbox(label="Status", interactive=False, value="Ready.")
180
+
181
+ scout_btn.click(
182
+ scout_demo,
183
+ inputs=[pasted_text],
184
+ outputs=[scout_output, scout_status],
185
+ )
186
+
187
+ # ─────────────────────────────────────────────────────────────────────
188
+ # Tab 2: Sage Demo
189
+ # ─────────────────────────────────────────────────────────────────────
190
+ with gr.TabItem("🦉 Sage — Socratic Clearing"):
191
+ gr.Markdown(
192
+ """
193
+ **Sage** guides you through a Socratic clearing session. Pick a concept and explain it in your own words.
194
+ """
195
+ )
196
+
197
+ concept_input = gr.Textbox(
198
+ label="Concept to clear",
199
+ placeholder="e.g., 'Binary Search'",
200
+ value="Recursion",
201
+ )
202
+
203
+ response_input = gr.Textbox(
204
+ label="Your explanation",
205
+ placeholder="Explain the concept in your own words. Be as detailed as you can.",
206
+ lines=4,
207
+ )
208
+
209
+ sage_btn = gr.Button("💭 Get Sage Response", size="lg")
210
+ sage_output = gr.Markdown()
211
+
212
+ sage_btn.click(
213
+ sage_demo,
214
+ inputs=[concept_input, response_input],
215
+ outputs=[sage_output],
216
+ )
217
+
218
+ # ─────────────────────────────────────────────────────────────────────
219
+ # Tab 3: Live Ledger
220
+ # ─────────────────────────────────────────────────────────────────────
221
+ with gr.TabItem("📖 Live Ledger"):
222
+ gr.Markdown(
223
+ """
224
+ This is your concept ledger. Every concept you study is tracked here with its status:
225
+ - **on_loan**: You pasted it but haven't cleared it yet.
226
+ - **clear**: You explained it to Sage and passed.
227
+ - **persists**: Lens found gaps, needs another session.
228
+ """
229
+ )
230
+
231
+ ledger_df = get_ledger_display()
232
+ ledger_table = gr.Dataframe(
233
+ value=ledger_df,
234
+ interactive=False,
235
+ wrap=True,
236
+ )
237
+
238
+ refresh_btn = gr.Button("🔄 Refresh Ledger")
239
+ refresh_btn.click(
240
+ lambda: get_ledger_display(),
241
+ outputs=[ledger_table],
242
+ )
243
+
244
+ # ─────────────────────────────────────────────────────────────────────
245
+ # Tab 4: Architecture & Setup
246
+ # ─────────────────────────────────────────────────────────────────────
247
+ with gr.TabItem("🏗️ Architecture"):
248
+ gr.Markdown(
249
+ """
250
+ ## System Design
251
+
252
+ **Backend Stack:**
253
+ - FastAPI for the web server
254
+ - SQLite for concept ledger persistence
255
+ - ChromaDB for curriculum context (RAG)
256
+ - Ollama for local Gemma inference
257
+ - Privacy-preserving sync with concept-level aggregates only
258
+
259
+ **Key Features:**
260
+ - Offline-first (no cloud dependency)
261
+ - Session fingerprinting to detect gaming behavior
262
+ - Anti-spoof scoring on Lens uploads
263
+ - No student identifiers in instructor reports
264
+
265
+ ## Quick Local Setup
266
+
267
+ ```bash
268
+ python3 -m venv .venv
269
+ source .venv/bin/activate
270
+ pip install -r requirements.txt
271
+
272
+ # Terminal 1: Start Ollama
273
+ ollama serve
274
+
275
+ # Terminal 2: Start the backend
276
+ python -m uvicorn knowledge.main:app --host 127.0.0.1 --port 8000
277
+
278
+ # Optional: Load curriculum material
279
+ python -m knowledge.vectorize path/to/course.pdf
280
+ ```
281
+
282
+ ## Deploy on Hugging Face Spaces
283
+
284
+ 1. Create a new Space with **Gradio** SDK
285
+ 2. Push this repo to the Space
286
+ 3. Set `KNOWLEDGE_API_URL` environment variable (optional, for live backend)
287
+ 4. Share the Space URL as your Kaggle demo link
288
+ """
289
+ )
290
+
291
+ # ─────────────────────────────────────────────────────────────────────
292
+ # Tab 5: For Judges
293
+ # ─────────────────────────────────────────────────────────────────────
294
+ with gr.TabItem("🎯 For Kaggle Judges"):
295
+ gr.Markdown(
296
+ """
297
+ ## What Makes KnowLedge Different
298
+
299
+ Most AI tutoring tools are **answer machines**. KnowLedge is a **verification system**.
300
+
301
+ ### The Problem
302
+ - Students copy AI-generated code without understanding
303
+ - Traditional quizzes can be gamed with a second AI
304
+ - Instructors have no way to detect this pattern
305
+
306
+ ### The KnowLedge Solution
307
+ - **Scout** logs every concept you borrow from AI
308
+ - **Sage** forces you to explain it yourself before you own it
309
+ - **Lens** checks handwritten work for logic gaps
310
+ - **Integrity** fingerprints your session to catch repeat gaming
311
+ - **Sync** gives instructors only concept-level aggregates (privacy-first)
312
+
313
+ ### Why This Matters
314
+ - Fixes a **real problem** in modern education
315
+ - Uses **Gemma 4** for both extraction and dialogue
316
+ - Runs **fully offline** for maximum privacy
317
+ - Provides a **zero-setup** public demo (this Space)
318
+
319
+ ### Key Metrics
320
+ - **Debt Score**: How much of your work is still "borrowed"
321
+ - **Spoof Attempts**: Session fingerprinting detects copy-paste patterns
322
+ - **Integrity Signals**: Anti-gaming scoring from Lens
323
+
324
+ This is what judges should look for. Not an answering machine. A **learning enforcer**.
325
+ """
326
+ )
327
+
328
+
329
+ # Launch the Gradio interface
330
+ if __name__ == "__main__":
331
+ demo.launch()
332
+ :root {
333
+ --bg: #f5f0e8;
334
+ --panel: #ffffff;
335
+ --ink: #1c1a14;
336
+ --muted: #7a7565;
337
+ --teal: #0e7a6e;
338
+ --amber: #d4820a;
339
+ --coral: #c94a3a;
340
+ --rule: rgba(28, 26, 20, 0.12);
341
+ }
342
+
343
+ .block-container {
344
+ padding-top: 2rem;
345
+ padding-bottom: 2rem;
346
+ }
347
+
348
+ .hero {
349
+ background: linear-gradient(135deg, rgba(14,122,110,0.10), rgba(212,130,10,0.10));
350
+ border: 1px solid var(--rule);
351
+ border-radius: 24px;
352
+ padding: 2rem;
353
+ margin-bottom: 1.25rem;
354
+ }
355
+
356
+ .hero h1 {
357
+ color: var(--ink);
358
+ font-size: 3rem;
359
+ line-height: 1;
360
+ margin-bottom: 0.5rem;
361
+ }
362
+
363
+ .hero p {
364
+ color: var(--muted);
365
+ font-size: 1.02rem;
366
+ line-height: 1.6;
367
+ max-width: 58rem;
368
+ }
369
+
370
+ .badge-row {
371
+ display: flex;
372
+ flex-wrap: wrap;
373
+ gap: 0.6rem;
374
+ margin-top: 1rem;
375
+ }
376
+
377
+ .badge {
378
+ display: inline-block;
379
+ padding: 0.35rem 0.65rem;
380
+ border-radius: 999px;
381
+ border: 1px solid var(--rule);
382
+ background: rgba(255,255,255,0.72);
383
+ color: var(--ink);
384
+ font-size: 0.85rem;
385
+ }
386
+
387
+ .card {
388
+ background: var(--panel);
389
+ border: 1px solid var(--rule);
390
+ border-radius: 18px;
391
+ padding: 1rem;
392
+ box-shadow: 0 10px 30px rgba(28, 26, 20, 0.06);
393
+ }
394
+
395
+ .metric {
396
+ background: white;
397
+ border: 1px solid var(--rule);
398
+ border-radius: 18px;
399
+ padding: 1rem;
400
+ min-height: 130px;
401
+ }
402
+
403
+ .metric-label {
404
+ color: var(--muted);
405
+ font-size: 0.82rem;
406
+ text-transform: uppercase;
407
+ letter-spacing: 0.08em;
408
+ margin-bottom: 0.35rem;
409
+ }
410
+
411
+ .metric-value {
412
+ color: var(--ink);
413
+ font-size: 2rem;
414
+ font-weight: 700;
415
+ line-height: 1;
416
+ margin-bottom: 0.35rem;
417
+ }
418
+
419
+ .metric-sub {
420
+ color: var(--muted);
421
+ font-size: 0.88rem;
422
+ line-height: 1.4;
423
+ }
424
+
425
+ .judge-box {
426
+ border-left: 4px solid var(--teal);
427
+ padding: 0.85rem 1rem;
428
+ background: rgba(14, 122, 110, 0.06);
429
+ border-radius: 12px;
430
+ }
431
+
432
+ .small-note {
433
+ color: var(--muted);
434
+ font-size: 0.92rem;
435
+ line-height: 1.55;
436
+ }
437
+
438
+ .codebox {
439
+ background: #1c1a14;
440
+ color: #f5f0e8;
441
+ border-radius: 14px;
442
+ padding: 1rem;
443
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
444
+ font-size: 0.9rem;
445
+ overflow-x: auto;
446
+ }
447
+ </style>
448
+ """
449
+
450
+
451
+ st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
452
+
453
+
454
+ @st.cache_data(show_spinner=False)
455
+ def backend_snapshot(base_url: str) -> Optional[Dict[str, Any]]:
456
+ base = base_url.rstrip("/")
457
+ try:
458
+ with httpx.Client(timeout=6.0) as client:
459
+ state = client.get(f"{base}/api/state").json()
460
+ sync = client.get(f"{base}/api/sync/status").json()
461
+ integrity = client.get(f"{base}/api/integrity/report").json()
462
+ return {"state": state, "sync": sync, "integrity": integrity}
463
+ except Exception:
464
+ return None
465
+
466
+
467
+ backend_url = os.environ.get("KNOWLEDGE_API_URL", "").strip()
468
+ space_mode = os.environ.get("SPACE_MODE", "demo").strip().lower()
469
+ snapshot = backend_snapshot(backend_url) if backend_url else None
470
+ use_live_data = snapshot is not None
471
+ ledger = snapshot["state"]["debts"] if use_live_data else SAMPLE_LEDGER
472
+ heatmap = snapshot["state"]["heatmap"] if use_live_data else SAMPLE_HEATMAP
473
+ sync_status = snapshot["sync"] if use_live_data else {"pending_count": SAMPLE_METRICS["pending_sync"], "last_sync": {"status": "sent"}}
474
+ integrity = snapshot["integrity"] if use_live_data else {"spoof_attempts": SAMPLE_METRICS["spoof_attempts"], "sessions_with_signatures": 8, "sessions_total": 10}
475
+
476
+ active = sum(1 for row in ledger if row.get("status") in {"on_loan", "persists"})
477
+ cleared = sum(1 for row in ledger if row.get("status") in {"clear", "owned"})
478
+ persists = sum(1 for row in ledger if row.get("status") == "persists")
479
+ debt_score = round(((active + persists) / max(len(ledger), 1)) * 100) if ledger else 0
480
+
481
+ st.markdown(
482
+ """
483
+ <div class="hero">
484
+ <h1>KnowLedge</h1>
485
+ <p>
486
+ A local-first learning verification system for the Gemma 4 for Good hackathon.
487
+ KnowLedge turns pasted AI-assisted work into a guided mastery loop: Scout extracts concepts,
488
+ Sage clears them through Socratic dialogue, and Lens verifies understanding with integrity checks.
489
+ </p>
490
+ <div class="badge-row">
491
+ <span class="badge">FastAPI backend</span>
492
+ <span class="badge">SQLite + ChromaDB</span>
493
+ <span class="badge">Ollama + Gemma</span>
494
+ <span class="badge">Privacy-preserving sync</span>
495
+ <span class="badge">Expo companion app</span>
496
+ </div>
497
+ </div>
498
+ """,
499
+ unsafe_allow_html=True,
500
+ )
501
+
502
+ if use_live_data:
503
+ st.success("Live backend connected. This Space can be used as a zero-setup demo front door.")
504
+ else:
505
+ if backend_url:
506
+ st.warning(f"Backend URL set, but the app could not reach {backend_url}. Showing demo data instead.")
507
+ else:
508
+ st.info("Demo mode is active. Add KNOWLEDGE_API_URL in Hugging Face Spaces to connect to your hosted backend.")
509
+
510
+ col1, col2, col3, col4, col5 = st.columns(5)
511
+ with col1:
512
+ st.markdown(f'<div class="metric"><div class="metric-label">Active Concepts</div><div class="metric-value">{active}</div><div class="metric-sub">currently on loan or persisting</div></div>', unsafe_allow_html=True)
513
+ with col2:
514
+ st.markdown(f'<div class="metric"><div class="metric-label">Cleared</div><div class="metric-value">{cleared}</div><div class="metric-sub">concepts owned by the student</div></div>', unsafe_allow_html=True)
515
+ with col3:
516
+ st.markdown(f'<div class="metric"><div class="metric-label">Debt Score</div><div class="metric-value">{debt_score}%</div><div class="metric-sub">lower is better</div></div>', unsafe_allow_html=True)
517
+ with col4:
518
+ st.markdown(f'<div class="metric"><div class="metric-label">Pending Sync</div><div class="metric-value">{sync_status.get("pending_count", 0)}</div><div class="metric-sub">weekly aggregate queue</div></div>', unsafe_allow_html=True)
519
+ with col5:
520
+ st.markdown(f'<div class="metric"><div class="metric-label">Spoof Signals</div><div class="metric-value">{integrity.get("spoof_attempts", 0)}</div><div class="metric-sub">anti-gaming checks</div></div>', unsafe_allow_html=True)
521
+
522
+ left, right = st.columns([1.15, 0.85], gap="large")
523
+
524
+ with left:
525
+ tabs = st.tabs(["Live Demo", "Architecture", "How to Run", "Judge Notes"])
526
+
527
+ with tabs[0]:
528
+ st.subheader("What judges can test")
529
+ st.write("KnowLedge is designed to show a complete learning-verification loop, not just a chatbot.")
530
+ st.markdown(
531
+ """
532
+ 1. Paste a concept or AI-assisted explanation.
533
+ 2. Scout extracts the concepts and logs them to the ledger.
534
+ 3. Sage asks clarifying questions until the concept is cleared.
535
+ 4. Lens and integrity checks keep the workflow honest.
536
+ 5. Reports share only anonymous aggregates.
537
+ """
538
+ )
539
+ demo_rows = pd.DataFrame(ledger)[["concept", "status", "confidence"]] if ledger else pd.DataFrame(columns=["concept", "status", "confidence"])
540
+ st.dataframe(demo_rows, use_container_width=True, hide_index=True)
541
+
542
+ with tabs[1]:
543
+ st.subheader("Product Architecture")
544
+ st.markdown(
545
+ """
546
+ - **Scout** finds concepts inside pasted text.
547
+ - **Sage** runs the Socratic clearing loop.
548
+ - **Lens** checks handwritten or visual reasoning.
549
+ - **Sync** exports only concept-level aggregates.
550
+ - **Integrity** uses fingerprints and anti-spoof scoring.
551
+ """
552
+ )
553
+
554
+
555
+ # Launch the Gradio interface
556
+ if __name__ == "__main__":
557
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ gradio
5
+ httpx
6
+ ollama
7
+ chromadb
8
+ pdfplumber
9
+ pillow
10
+ pandas
11
+ jinja2
12
+ python-dotenv