Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +15 -0
- KnowledgeValueLab.md +430 -0
- README.md +40 -4
- app.py +508 -0
- kvl/__init__.py +0 -0
- kvl/config.py +184 -0
- kvl/ingestor.py +80 -0
- kvl/modules/__init__.py +0 -0
- kvl/modules/attribution.py +109 -0
- kvl/modules/demand.py +124 -0
- kvl/modules/generation.py +143 -0
- kvl/modules/novelty.py +122 -0
- kvl/modules/retrieval.py +132 -0
- kvl/report.py +213 -0
- kvl/scorer.py +96 -0
- requirements.txt +7 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["streamlit", "run", "app.py", \
|
| 13 |
+
"--server.port=7860", \
|
| 14 |
+
"--server.address=0.0.0.0", \
|
| 15 |
+
"--server.headless=true"]
|
KnowledgeValueLab.md
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Knowledge Value Lab (KVL)
|
| 2 |
+
|
| 3 |
+
## A Framework for Measuring the Marginal Value of Knowledge Assets for AI Systems
|
| 4 |
+
|
| 5 |
+
### Concept Note and Implementation Plan
|
| 6 |
+
|
| 7 |
+
## Executive Summary
|
| 8 |
+
|
| 9 |
+
Organizations around the world are investing heavily in creating, curating, digitizing, licensing, and publishing knowledge assets for use in artificial intelligence systems.
|
| 10 |
+
|
| 11 |
+
These assets include:
|
| 12 |
+
|
| 13 |
+
* Research papers
|
| 14 |
+
* Technical reports
|
| 15 |
+
* Books
|
| 16 |
+
* Policy documents
|
| 17 |
+
* Government publications
|
| 18 |
+
* Educational materials
|
| 19 |
+
* Domain-specific knowledge bases
|
| 20 |
+
* Datasets
|
| 21 |
+
* Web archives
|
| 22 |
+
|
| 23 |
+
Despite growing investments in AI-ready content, there is currently no widely accepted method for answering a fundamental question:
|
| 24 |
+
|
| 25 |
+
**How much value does a newly available knowledge asset contribute to AI systems?**
|
| 26 |
+
|
| 27 |
+
A document may contain information that is already embedded in existing foundation models, in which case its marginal contribution is small. Alternatively, it may contain unique knowledge that significantly improves retrieval systems, AI assistants, decision-support tools, and downstream applications.
|
| 28 |
+
|
| 29 |
+
Knowledge Value Lab (KVL) is a proposed framework and software platform designed to measure the marginal value of knowledge assets for AI systems. The platform evaluates individual documents, datasets, repositories, and collections using a standardized methodology that combines knowledge novelty, retrieval performance, answer quality, grounding, and user demand.
|
| 30 |
+
|
| 31 |
+
The result is a transparent and reproducible "Knowledge Value Score" that quantifies the contribution of information assets to AI ecosystems.
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
# 1. Motivation
|
| 36 |
+
|
| 37 |
+
The emergence of foundation models has fundamentally changed how knowledge is consumed and utilized.
|
| 38 |
+
|
| 39 |
+
Historically, the value of a document was measured through indicators such as:
|
| 40 |
+
|
| 41 |
+
* Citations
|
| 42 |
+
* Downloads
|
| 43 |
+
* Sales
|
| 44 |
+
* Views
|
| 45 |
+
* Academic impact
|
| 46 |
+
|
| 47 |
+
These measures provide limited insight into the role of knowledge within AI systems.
|
| 48 |
+
|
| 49 |
+
A document that is rarely cited may substantially improve an AI assistant's ability to answer questions. Conversely, a highly cited document may contribute little additional value if its contents are already extensively represented within existing training corpora.
|
| 50 |
+
|
| 51 |
+
Organizations increasingly face decisions regarding:
|
| 52 |
+
|
| 53 |
+
* Which content should be digitized?
|
| 54 |
+
* Which repositories should be prioritized?
|
| 55 |
+
* Which datasets deserve funding?
|
| 56 |
+
* Which knowledge assets should be licensed for AI applications?
|
| 57 |
+
* Which public data investments create the greatest societal return?
|
| 58 |
+
|
| 59 |
+
KVL seeks to provide evidence-based answers to these questions.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
# 2. Vision
|
| 64 |
+
|
| 65 |
+
To create a standardized framework for measuring the contribution of knowledge assets to AI systems, enabling informed decisions about data investments, content sharing, and knowledge infrastructure development.
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
# 3. Core Research Questions
|
| 70 |
+
|
| 71 |
+
### Knowledge Novelty
|
| 72 |
+
|
| 73 |
+
How much information contained in an asset is already known by contemporary AI models?
|
| 74 |
+
|
| 75 |
+
### Retrieval Utility
|
| 76 |
+
|
| 77 |
+
How much does the asset improve information retrieval systems?
|
| 78 |
+
|
| 79 |
+
### Generation Utility
|
| 80 |
+
|
| 81 |
+
How much does the asset improve AI-generated responses?
|
| 82 |
+
|
| 83 |
+
### Attribution Utility
|
| 84 |
+
|
| 85 |
+
Can improvements be directly attributed to the asset?
|
| 86 |
+
|
| 87 |
+
### Demand Utility
|
| 88 |
+
|
| 89 |
+
How frequently is the knowledge needed by users?
|
| 90 |
+
|
| 91 |
+
### Social Utility
|
| 92 |
+
|
| 93 |
+
What societal value may arise from making the knowledge available?
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
# 4. Conceptual Framework
|
| 98 |
+
|
| 99 |
+
KVL treats every knowledge asset as a potential contributor to AI capability.
|
| 100 |
+
|
| 101 |
+
The framework estimates value across five dimensions.
|
| 102 |
+
|
| 103 |
+
## Dimension 1: Knowledge Novelty
|
| 104 |
+
|
| 105 |
+
Measures whether the information contained within a document is already represented in existing AI models.
|
| 106 |
+
|
| 107 |
+
Examples:
|
| 108 |
+
|
| 109 |
+
* Recently published research
|
| 110 |
+
* Local knowledge
|
| 111 |
+
* Proprietary content
|
| 112 |
+
* Specialized technical documentation
|
| 113 |
+
* Low-resource language materials
|
| 114 |
+
|
| 115 |
+
may receive high novelty scores.
|
| 116 |
+
|
| 117 |
+
Widely distributed information may receive lower scores.
|
| 118 |
+
|
| 119 |
+
### Outputs
|
| 120 |
+
|
| 121 |
+
Knowledge Novelty Score
|
| 122 |
+
|
| 123 |
+
0β100
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## Dimension 2: Retrieval Utility
|
| 128 |
+
|
| 129 |
+
Measures whether the asset improves search and retrieval systems.
|
| 130 |
+
|
| 131 |
+
Typical evaluation metrics include:
|
| 132 |
+
|
| 133 |
+
* Recall@K
|
| 134 |
+
* Mean Reciprocal Rank
|
| 135 |
+
* nDCG
|
| 136 |
+
* Context Precision
|
| 137 |
+
* Context Recall
|
| 138 |
+
|
| 139 |
+
### Outputs
|
| 140 |
+
|
| 141 |
+
Retrieval Utility Score
|
| 142 |
+
|
| 143 |
+
0β100
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## Dimension 3: Generation Utility
|
| 148 |
+
|
| 149 |
+
Measures whether access to the asset improves AI-generated outputs.
|
| 150 |
+
|
| 151 |
+
Applications include:
|
| 152 |
+
|
| 153 |
+
* Question answering
|
| 154 |
+
* Summarization
|
| 155 |
+
* Advisory systems
|
| 156 |
+
* Research assistants
|
| 157 |
+
* Educational tutors
|
| 158 |
+
* Enterprise knowledge assistants
|
| 159 |
+
|
| 160 |
+
Evaluation criteria include:
|
| 161 |
+
|
| 162 |
+
* Accuracy
|
| 163 |
+
* Completeness
|
| 164 |
+
* Specificity
|
| 165 |
+
* Relevance
|
| 166 |
+
* Actionability
|
| 167 |
+
* Safety
|
| 168 |
+
|
| 169 |
+
### Outputs
|
| 170 |
+
|
| 171 |
+
Generation Utility Score
|
| 172 |
+
|
| 173 |
+
0β100
|
| 174 |
+
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
## Dimension 4: Attribution and Grounding
|
| 178 |
+
|
| 179 |
+
Measures whether observed improvements genuinely originate from the asset.
|
| 180 |
+
|
| 181 |
+
Key questions include:
|
| 182 |
+
|
| 183 |
+
* Is the document being retrieved?
|
| 184 |
+
* Is evidence from the document being used?
|
| 185 |
+
* Are generated outputs properly grounded?
|
| 186 |
+
|
| 187 |
+
### Outputs
|
| 188 |
+
|
| 189 |
+
Grounding Score
|
| 190 |
+
|
| 191 |
+
0β100
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## Dimension 5: Demand Utility
|
| 196 |
+
|
| 197 |
+
Measures the practical importance of the knowledge.
|
| 198 |
+
|
| 199 |
+
Examples include:
|
| 200 |
+
|
| 201 |
+
* Frequency of related user queries
|
| 202 |
+
* Coverage of unmet information needs
|
| 203 |
+
* Relevance to priority domains
|
| 204 |
+
* Geographic relevance
|
| 205 |
+
* Language coverage
|
| 206 |
+
|
| 207 |
+
### Outputs
|
| 208 |
+
|
| 209 |
+
Demand Utility Score
|
| 210 |
+
|
| 211 |
+
0β100
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
# 5. Knowledge Value Score
|
| 216 |
+
|
| 217 |
+
The overall score combines all dimensions into a single measure.
|
| 218 |
+
|
| 219 |
+
KVS =
|
| 220 |
+
|
| 221 |
+
30% Knowledge Novelty
|
| 222 |
+
|
| 223 |
+
20% Retrieval Utility
|
| 224 |
+
|
| 225 |
+
25% Generation Utility
|
| 226 |
+
|
| 227 |
+
15% Grounding Utility
|
| 228 |
+
|
| 229 |
+
10% Demand Utility
|
| 230 |
+
|
| 231 |
+
Result:
|
| 232 |
+
|
| 233 |
+
0β100
|
| 234 |
+
|
| 235 |
+
Classification:
|
| 236 |
+
|
| 237 |
+
0β20 Minimal Value
|
| 238 |
+
|
| 239 |
+
21β40 Incremental Value
|
| 240 |
+
|
| 241 |
+
41β60 Moderate Value
|
| 242 |
+
|
| 243 |
+
61β80 High Value
|
| 244 |
+
|
| 245 |
+
81β100 Transformational Value
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
# 6. System Architecture
|
| 250 |
+
|
| 251 |
+
## Module A: Knowledge Novelty Engine
|
| 252 |
+
|
| 253 |
+
Functions:
|
| 254 |
+
|
| 255 |
+
* Claim extraction
|
| 256 |
+
* Question generation
|
| 257 |
+
* Closed-book model evaluation
|
| 258 |
+
* Cross-model comparison
|
| 259 |
+
* Novelty estimation
|
| 260 |
+
|
| 261 |
+
Outputs:
|
| 262 |
+
|
| 263 |
+
Knowledge Novelty Score
|
| 264 |
+
|
| 265 |
+
---
|
| 266 |
+
|
| 267 |
+
## Module B: Retrieval Evaluation Engine
|
| 268 |
+
|
| 269 |
+
Functions:
|
| 270 |
+
|
| 271 |
+
* Index creation
|
| 272 |
+
* Retrieval benchmarking
|
| 273 |
+
* Search quality assessment
|
| 274 |
+
* Comparative experiments
|
| 275 |
+
|
| 276 |
+
Outputs:
|
| 277 |
+
|
| 278 |
+
Retrieval Utility Score
|
| 279 |
+
|
| 280 |
+
---
|
| 281 |
+
|
| 282 |
+
## Module C: Generation Evaluation Engine
|
| 283 |
+
|
| 284 |
+
Functions:
|
| 285 |
+
|
| 286 |
+
* Response generation
|
| 287 |
+
* Multi-model testing
|
| 288 |
+
* Quality assessment
|
| 289 |
+
* Human and AI judging
|
| 290 |
+
|
| 291 |
+
Outputs:
|
| 292 |
+
|
| 293 |
+
Generation Utility Score
|
| 294 |
+
|
| 295 |
+
---
|
| 296 |
+
|
| 297 |
+
## Module D: Attribution Engine
|
| 298 |
+
|
| 299 |
+
Functions:
|
| 300 |
+
|
| 301 |
+
* Citation analysis
|
| 302 |
+
* Evidence tracing
|
| 303 |
+
* Source attribution
|
| 304 |
+
* Grounding verification
|
| 305 |
+
|
| 306 |
+
Outputs:
|
| 307 |
+
|
| 308 |
+
Grounding Score
|
| 309 |
+
|
| 310 |
+
---
|
| 311 |
+
|
| 312 |
+
## Module E: Demand Analysis Engine
|
| 313 |
+
|
| 314 |
+
Functions:
|
| 315 |
+
|
| 316 |
+
* Query log analysis
|
| 317 |
+
* Topic modeling
|
| 318 |
+
* Gap detection
|
| 319 |
+
* User demand estimation
|
| 320 |
+
|
| 321 |
+
Outputs:
|
| 322 |
+
|
| 323 |
+
Demand Utility Score
|
| 324 |
+
|
| 325 |
+
---
|
| 326 |
+
|
| 327 |
+
# 7. User Experience
|
| 328 |
+
|
| 329 |
+
Users upload:
|
| 330 |
+
|
| 331 |
+
* PDF
|
| 332 |
+
* Word documents
|
| 333 |
+
* Web pages
|
| 334 |
+
* Datasets
|
| 335 |
+
* Knowledge collections
|
| 336 |
+
|
| 337 |
+
The platform automatically:
|
| 338 |
+
|
| 339 |
+
1. Ingests content
|
| 340 |
+
2. Extracts claims
|
| 341 |
+
3. Generates evaluation tasks
|
| 342 |
+
4. Executes experiments
|
| 343 |
+
5. Computes scores
|
| 344 |
+
6. Produces a report
|
| 345 |
+
|
| 346 |
+
Typical runtime:
|
| 347 |
+
|
| 348 |
+
Minutes to hours depending on corpus size.
|
| 349 |
+
|
| 350 |
+
---
|
| 351 |
+
|
| 352 |
+
# 8. Dashboard Outputs
|
| 353 |
+
|
| 354 |
+
The platform generates a Knowledge Value Report containing:
|
| 355 |
+
|
| 356 |
+
### Overall Knowledge Value Score
|
| 357 |
+
|
| 358 |
+
### Knowledge Novelty Assessment
|
| 359 |
+
|
| 360 |
+
### Retrieval Impact Analysis
|
| 361 |
+
|
| 362 |
+
### Generation Impact Analysis
|
| 363 |
+
|
| 364 |
+
### Attribution Assessment
|
| 365 |
+
|
| 366 |
+
### Demand Analysis
|
| 367 |
+
|
| 368 |
+
### Recommended Actions
|
| 369 |
+
|
| 370 |
+
Examples:
|
| 371 |
+
|
| 372 |
+
* Publish openly
|
| 373 |
+
* Prioritize indexing
|
| 374 |
+
* Translate into additional languages
|
| 375 |
+
* Integrate into retrieval systems
|
| 376 |
+
* Acquire licensing rights
|
| 377 |
+
* Merge with related collections
|
| 378 |
+
|
| 379 |
+
---
|
| 380 |
+
|
| 381 |
+
# 9. Extension to Repository-Level Evaluation
|
| 382 |
+
|
| 383 |
+
The framework can be applied to:
|
| 384 |
+
|
| 385 |
+
* Digital libraries
|
| 386 |
+
* Academic repositories
|
| 387 |
+
* Government archives
|
| 388 |
+
* Corporate knowledge bases
|
| 389 |
+
* Publisher collections
|
| 390 |
+
* Data commons
|
| 391 |
+
* Open data platforms
|
| 392 |
+
|
| 393 |
+
This enables comparative analyses such as:
|
| 394 |
+
|
| 395 |
+
* Which repository contributes the most novel knowledge?
|
| 396 |
+
* Which collection generates the largest gains in AI performance?
|
| 397 |
+
* Which public data investments generate the greatest value?
|
| 398 |
+
|
| 399 |
+
---
|
| 400 |
+
|
| 401 |
+
# 10. Social Return on Knowledge
|
| 402 |
+
|
| 403 |
+
An optional extension estimates downstream societal value.
|
| 404 |
+
|
| 405 |
+
Knowledge assets are evaluated not only by their impact on AI performance but also by their contribution to real-world outcomes.
|
| 406 |
+
|
| 407 |
+
Examples:
|
| 408 |
+
|
| 409 |
+
Document β Improved AI Output β Better Decision β Improved Outcome
|
| 410 |
+
|
| 411 |
+
Possible outcome domains include:
|
| 412 |
+
|
| 413 |
+
* Education
|
| 414 |
+
* Healthcare
|
| 415 |
+
* Agriculture
|
| 416 |
+
* Public administration
|
| 417 |
+
* Climate adaptation
|
| 418 |
+
* Scientific research
|
| 419 |
+
|
| 420 |
+
This extension enables estimation of a Social Return on Knowledge (SRK) score.
|
| 421 |
+
|
| 422 |
+
---
|
| 423 |
+
|
| 424 |
+
# 11. Long-Term Vision
|
| 425 |
+
|
| 426 |
+
Knowledge Value Lab aims to become a standard for measuring the value of knowledge in the AI era.
|
| 427 |
+
|
| 428 |
+
Just as citation metrics transformed scholarly communication and web analytics transformed digital publishing, KVL seeks to establish a new class of metrics that quantify how knowledge contributes to artificial intelligence systems.
|
| 429 |
+
|
| 430 |
+
The ultimate goal is to enable governments, publishers, researchers, funders, and technology developers to make evidence-based decisions about the creation, sharing, preservation, and financing of knowledge assets in a world increasingly mediated by AI.
|
README.md
CHANGED
|
@@ -1,10 +1,46 @@
|
|
| 1 |
---
|
| 2 |
title: Knowledge Value Lab
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Knowledge Value Lab
|
| 3 |
+
emoji: π¬
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
license: apache-2.0
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Knowledge Value Lab (KVL)
|
| 13 |
+
|
| 14 |
+
**Measuring the Marginal Value of Knowledge Assets for AI Systems**
|
| 15 |
+
|
| 16 |
+
KVL quantifies how much a knowledge document contributes to AI systems across five dimensions, producing a single weighted **Knowledge Value Score (KVS)**.
|
| 17 |
+
|
| 18 |
+
## How to Use
|
| 19 |
+
|
| 20 |
+
1. Upload a Markdown (`.md`) document
|
| 21 |
+
2. Click **Evaluate Knowledge Value**
|
| 22 |
+
3. Review the scored report and download it
|
| 23 |
+
|
| 24 |
+
## Dimensions
|
| 25 |
+
|
| 26 |
+
| Dimension | Weight | What it measures |
|
| 27 |
+
|---|---|---|
|
| 28 |
+
| Knowledge Novelty | 30% | How much of the document is unknown to the base model |
|
| 29 |
+
| Retrieval Utility | 20% | How well the document surfaces in RAG search |
|
| 30 |
+
| Generation Utility | 25% | How much RAG answers improve over baseline |
|
| 31 |
+
| Attribution & Grounding | 15% | How faithfully answers are grounded in the document |
|
| 32 |
+
| Demand Utility | 10% | How frequently this knowledge is needed by users |
|
| 33 |
+
|
| 34 |
+
## Score Classifications
|
| 35 |
+
|
| 36 |
+
| Score | Classification |
|
| 37 |
+
|---|---|
|
| 38 |
+
| 81β100 | Transformational Value |
|
| 39 |
+
| 61β80 | High Value |
|
| 40 |
+
| 41β60 | Moderate Value |
|
| 41 |
+
| 21β40 | Incremental Value |
|
| 42 |
+
| 0β20 | Minimal Value |
|
| 43 |
+
|
| 44 |
+
## Important Note
|
| 45 |
+
|
| 46 |
+
Knowledge Novelty and Generation Utility scores are **model-relative** β they measure value against specific AI models and will change when models are updated. Always report scores alongside the model names and evaluation date shown in each report.
|
app.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Knowledge Value Lab β Streamlit prototype."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import os
|
| 5 |
+
import time
|
| 6 |
+
import anthropic
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
from kvl import ingestor, scorer, report
|
| 14 |
+
from kvl.modules import novelty, retrieval, generation, attribution, demand
|
| 15 |
+
from kvl.config import DIMENSION_META, KVS_CLASSIFICATION, MODELS, SENSITIVITY_COLOR, model_meta
|
| 16 |
+
|
| 17 |
+
# ββ Page config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
+
st.set_page_config(
|
| 19 |
+
page_title="Knowledge Value Lab",
|
| 20 |
+
page_icon="π¬",
|
| 21 |
+
layout="wide",
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# ββ CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
st.markdown("""
|
| 26 |
+
<style>
|
| 27 |
+
.kvs-box {
|
| 28 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%);
|
| 29 |
+
border-radius: 12px;
|
| 30 |
+
padding: 28px 36px;
|
| 31 |
+
text-align: center;
|
| 32 |
+
margin-bottom: 8px;
|
| 33 |
+
}
|
| 34 |
+
.kvs-number { font-size: 64px; font-weight: 800; color: #f0f4ff; line-height: 1; }
|
| 35 |
+
.kvs-label { font-size: 16px; color: #8ab4f8; margin-top: 4px; }
|
| 36 |
+
.kvs-class { font-size: 22px; font-weight: 600; margin-top: 8px; }
|
| 37 |
+
.kvs-meta { font-size: 12px; color: #556; margin-top: 10px; font-family: monospace; }
|
| 38 |
+
.model-chip {
|
| 39 |
+
display: inline-block;
|
| 40 |
+
background: #1a2740;
|
| 41 |
+
border: 1px solid #2a4060;
|
| 42 |
+
border-radius: 4px;
|
| 43 |
+
padding: 2px 8px;
|
| 44 |
+
font-size: 11px;
|
| 45 |
+
font-family: monospace;
|
| 46 |
+
color: #8ab4f8;
|
| 47 |
+
margin: 2px;
|
| 48 |
+
}
|
| 49 |
+
.sens-badge {
|
| 50 |
+
display: inline-block;
|
| 51 |
+
border-radius: 4px;
|
| 52 |
+
padding: 1px 7px;
|
| 53 |
+
font-size: 11px;
|
| 54 |
+
font-weight: 600;
|
| 55 |
+
}
|
| 56 |
+
.warn-box {
|
| 57 |
+
background: #1a1500;
|
| 58 |
+
border-left: 3px solid #f8961e;
|
| 59 |
+
border-radius: 4px;
|
| 60 |
+
padding: 10px 14px;
|
| 61 |
+
font-size: 13px;
|
| 62 |
+
margin: 8px 0 16px 0;
|
| 63 |
+
color: #fff;
|
| 64 |
+
}
|
| 65 |
+
</style>
|
| 66 |
+
""", unsafe_allow_html=True)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
+
with st.sidebar:
|
| 71 |
+
st.markdown("## π¬ Knowledge Value Lab")
|
| 72 |
+
st.markdown(
|
| 73 |
+
"KVL measures the **marginal value** of a knowledge document to an AI system "
|
| 74 |
+
"across five independent dimensions, producing a single weighted **Knowledge Value Score (KVS)**."
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
st.divider()
|
| 78 |
+
st.markdown("### Models Used")
|
| 79 |
+
for key, m in MODELS.items():
|
| 80 |
+
st.markdown(
|
| 81 |
+
f"<span class='model-chip'>{m['display']}</span> \n"
|
| 82 |
+
f"<span style='font-size:11px;color:#888;'>{m['role']}</span>",
|
| 83 |
+
unsafe_allow_html=True,
|
| 84 |
+
)
|
| 85 |
+
st.markdown("")
|
| 86 |
+
|
| 87 |
+
st.markdown(
|
| 88 |
+
"<div class='warn-box'>"
|
| 89 |
+
"β οΈ <strong>Scores are model-relative.</strong> "
|
| 90 |
+
"Knowledge Novelty and Generation Utility reflect this document's value "
|
| 91 |
+
"to the <em>specific models above</em>. Scores will change when models are updated. "
|
| 92 |
+
"Always report scores alongside the model names and evaluation date."
|
| 93 |
+
"</div>",
|
| 94 |
+
unsafe_allow_html=True,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
st.divider()
|
| 98 |
+
st.markdown("### Score Classifications")
|
| 99 |
+
for threshold, label, desc in KVS_CLASSIFICATION:
|
| 100 |
+
hi = threshold + 19 if threshold < 81 else 100
|
| 101 |
+
st.markdown(f"**{threshold}β{hi}** β {label}")
|
| 102 |
+
st.caption(desc)
|
| 103 |
+
|
| 104 |
+
st.divider()
|
| 105 |
+
st.markdown("### Metric Guide")
|
| 106 |
+
for key, dmeta in DIMENSION_META.items():
|
| 107 |
+
sens = dmeta["model_sensitivity"]
|
| 108 |
+
sc = SENSITIVITY_COLOR[sens]
|
| 109 |
+
with st.expander(f"{dmeta['name']} Β· {int(dmeta['weight']*100)}%"):
|
| 110 |
+
st.markdown(
|
| 111 |
+
f"<span class='sens-badge' style='background:{sc}22;color:{sc};border:1px solid {sc}55;'>"
|
| 112 |
+
f"Model sensitivity: {sens}</span>",
|
| 113 |
+
unsafe_allow_html=True,
|
| 114 |
+
)
|
| 115 |
+
st.markdown(dmeta["description"])
|
| 116 |
+
st.markdown(f"**How measured:** {dmeta['how_measured']}")
|
| 117 |
+
st.markdown(f"*{dmeta['sensitivity_note']}*")
|
| 118 |
+
st.markdown(f"**High score:** {dmeta['high_means']}")
|
| 119 |
+
st.markdown(f"**Low score:** {dmeta['low_means']}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 123 |
+
st.title("π¬ Knowledge Value Lab")
|
| 124 |
+
st.markdown(
|
| 125 |
+
"**Measuring the Marginal Value of Knowledge Assets for AI Systems** \n"
|
| 126 |
+
"Upload a Markdown document to receive a quantified Knowledge Value Score across five dimensions."
|
| 127 |
+
)
|
| 128 |
+
st.divider()
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ββ Cached resources ββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββ
|
| 132 |
+
@st.cache_resource(show_spinner="Loading embedding model (all-MiniLM-L6-v2)...")
|
| 133 |
+
def load_embedder():
|
| 134 |
+
from sentence_transformers import SentenceTransformer
|
| 135 |
+
return SentenceTransformer("all-MiniLM-L6-v2")
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@st.cache_resource
|
| 139 |
+
def load_client():
|
| 140 |
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
| 141 |
+
if not api_key:
|
| 142 |
+
st.error("ANTHROPIC_API_KEY not found. Add it to your .env file.")
|
| 143 |
+
st.stop()
|
| 144 |
+
return anthropic.Anthropic(api_key=api_key)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ββ Upload section ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 148 |
+
col_upload, col_preview = st.columns([1, 1], gap="large")
|
| 149 |
+
|
| 150 |
+
with col_upload:
|
| 151 |
+
st.markdown("### Upload Document")
|
| 152 |
+
uploaded = st.file_uploader(
|
| 153 |
+
"Choose a Markdown file",
|
| 154 |
+
type=["md"],
|
| 155 |
+
help="Upload a .md file to evaluate its knowledge value for AI systems.",
|
| 156 |
+
label_visibility="collapsed",
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
if uploaded:
|
| 160 |
+
md_text = uploaded.read().decode("utf-8")
|
| 161 |
+
doc = ingestor.parse(md_text)
|
| 162 |
+
st.success(
|
| 163 |
+
f"**{doc.title}** \n"
|
| 164 |
+
f"{doc.word_count:,} words Β· {len(doc.sections)} sections Β· {len(doc.chunks)} chunks"
|
| 165 |
+
)
|
| 166 |
+
run = st.button("βΆ Evaluate Knowledge Value", type="primary", use_container_width=True)
|
| 167 |
+
else:
|
| 168 |
+
st.info("Drag and drop a `.md` file above, or click to browse.")
|
| 169 |
+
run = False
|
| 170 |
+
|
| 171 |
+
with col_preview:
|
| 172 |
+
st.markdown("### Document Preview")
|
| 173 |
+
if uploaded:
|
| 174 |
+
preview_text = md_text[:1200] + ("β¦" if len(md_text) > 1200 else "")
|
| 175 |
+
st.markdown(
|
| 176 |
+
f"<div style='background:#0e1117;border:1px solid #2a2a3a;border-radius:8px;"
|
| 177 |
+
f"padding:16px;font-size:13px;max-height:280px;overflow:auto;white-space:pre-wrap;color:#fff;'>"
|
| 178 |
+
f"{preview_text}</div>",
|
| 179 |
+
unsafe_allow_html=True,
|
| 180 |
+
)
|
| 181 |
+
else:
|
| 182 |
+
st.markdown(
|
| 183 |
+
"<div style='background:#0e1117;border:1px solid #2a2a3a;border-radius:8px;"
|
| 184 |
+
"padding:40px;text-align:center;color:#555;'>No document uploaded yet</div>",
|
| 185 |
+
unsafe_allow_html=True,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# ββ Evaluation pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 190 |
+
if run and uploaded:
|
| 191 |
+
st.divider()
|
| 192 |
+
st.markdown("### Evaluation in Progress")
|
| 193 |
+
|
| 194 |
+
client = load_client()
|
| 195 |
+
embedder = load_embedder()
|
| 196 |
+
|
| 197 |
+
steps = [
|
| 198 |
+
"Module A: Knowledge Novelty",
|
| 199 |
+
"Module B: Retrieval Utility",
|
| 200 |
+
"Module C: Generation Utility",
|
| 201 |
+
"Module D: Attribution & Grounding",
|
| 202 |
+
"Module E: Demand Utility",
|
| 203 |
+
"Computing Knowledge Value Score",
|
| 204 |
+
]
|
| 205 |
+
progress_bar = st.progress(0)
|
| 206 |
+
status_text = st.empty()
|
| 207 |
+
step_statuses = {s: "pending" for s in steps}
|
| 208 |
+
step_container = st.container()
|
| 209 |
+
|
| 210 |
+
def render_steps():
|
| 211 |
+
with step_container:
|
| 212 |
+
for s, state in step_statuses.items():
|
| 213 |
+
icon = {"pending": "β", "running": "β³", "done": "β"}[state]
|
| 214 |
+
color = {"pending": "#555", "running": "#8ab4f8", "done": "#4caf87"}[state]
|
| 215 |
+
st.markdown(
|
| 216 |
+
f"<span style='color:{color};font-family:monospace;'>{icon} {s}</span>",
|
| 217 |
+
unsafe_allow_html=True,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
def sub_progress(msg: str):
|
| 221 |
+
status_text.markdown(f"*{msg}*")
|
| 222 |
+
|
| 223 |
+
module_results = {}
|
| 224 |
+
eval_start = datetime.now()
|
| 225 |
+
t0 = time.time()
|
| 226 |
+
|
| 227 |
+
step_statuses[steps[0]] = "running"; render_steps(); progress_bar.progress(5)
|
| 228 |
+
module_results["novelty"] = novelty.evaluate(client, doc, progress_cb=sub_progress)
|
| 229 |
+
step_statuses[steps[0]] = "done"; progress_bar.progress(20)
|
| 230 |
+
|
| 231 |
+
step_statuses[steps[1]] = "running"; render_steps()
|
| 232 |
+
module_results["retrieval"] = retrieval.evaluate(client, doc, embedder, progress_cb=sub_progress)
|
| 233 |
+
step_statuses[steps[1]] = "done"; progress_bar.progress(40)
|
| 234 |
+
|
| 235 |
+
step_statuses[steps[2]] = "running"; render_steps()
|
| 236 |
+
module_results["generation"] = generation.evaluate(client, doc, progress_cb=sub_progress)
|
| 237 |
+
step_statuses[steps[2]] = "done"; progress_bar.progress(60)
|
| 238 |
+
|
| 239 |
+
step_statuses[steps[3]] = "running"; render_steps()
|
| 240 |
+
module_results["attribution"] = attribution.evaluate(
|
| 241 |
+
client, doc, module_results["generation"], embedder, progress_cb=sub_progress
|
| 242 |
+
)
|
| 243 |
+
step_statuses[steps[3]] = "done"; progress_bar.progress(80)
|
| 244 |
+
|
| 245 |
+
step_statuses[steps[4]] = "running"; render_steps()
|
| 246 |
+
module_results["demand"] = demand.evaluate(client, doc, progress_cb=sub_progress)
|
| 247 |
+
step_statuses[steps[4]] = "done"; progress_bar.progress(92)
|
| 248 |
+
|
| 249 |
+
step_statuses[steps[5]] = "running"; render_steps()
|
| 250 |
+
status_text.markdown("*Computing weighted Knowledge Value Score...*")
|
| 251 |
+
dim_scores = {k: module_results[k]["score"] for k in module_results}
|
| 252 |
+
kvs_result = scorer.compute(dim_scores)
|
| 253 |
+
step_statuses[steps[5]] = "done"; progress_bar.progress(100)
|
| 254 |
+
render_steps()
|
| 255 |
+
|
| 256 |
+
elapsed = round(time.time() - t0)
|
| 257 |
+
status_text.markdown(f"*Evaluation complete in {elapsed}s.*")
|
| 258 |
+
eval_date_str = eval_start.strftime("%Y-%m-%d %H:%M UTC")
|
| 259 |
+
meta = model_meta(eval_date_str)
|
| 260 |
+
|
| 261 |
+
# ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 262 |
+
st.divider()
|
| 263 |
+
st.markdown("## Knowledge Value Report")
|
| 264 |
+
|
| 265 |
+
kvs = kvs_result["kvs"]
|
| 266 |
+
classification = kvs_result["classification"]
|
| 267 |
+
color_map = {
|
| 268 |
+
"Transformational Value": "#ffd166",
|
| 269 |
+
"High Value": "#06d6a0",
|
| 270 |
+
"Moderate Value": "#8ab4f8",
|
| 271 |
+
"Incremental Value": "#f8961e",
|
| 272 |
+
"Minimal Value": "#ef476f",
|
| 273 |
+
}
|
| 274 |
+
badge_color = color_map.get(classification, "#8ab4f8")
|
| 275 |
+
|
| 276 |
+
# KVS hero with model metadata
|
| 277 |
+
st.markdown(
|
| 278 |
+
f"""<div class="kvs-box">
|
| 279 |
+
<div class="kvs-number">{kvs}</div>
|
| 280 |
+
<div class="kvs-label">Knowledge Value Score / 100</div>
|
| 281 |
+
<div class="kvs-class" style="color:{badge_color};">{classification}</div>
|
| 282 |
+
<div class="kvs-meta">
|
| 283 |
+
Evaluated {eval_date_str}<br>
|
| 284 |
+
Judge: {MODELS['judge']['display']} Β·
|
| 285 |
+
Worker: {MODELS['worker']['display']} Β·
|
| 286 |
+
Embeddings: {MODELS['embedder']['display']}
|
| 287 |
+
</div>
|
| 288 |
+
</div>""",
|
| 289 |
+
unsafe_allow_html=True,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Model-relativity warning
|
| 293 |
+
st.markdown(
|
| 294 |
+
"<div class='warn-box'>"
|
| 295 |
+
"β οΈ <strong>Score validity:</strong> "
|
| 296 |
+
"Knowledge Novelty and Generation Utility are <strong>model-relative</strong> β "
|
| 297 |
+
"they reflect this document's marginal value to the models listed above. "
|
| 298 |
+
"Scores will change if the underlying models are updated or replaced. "
|
| 299 |
+
"Always report scores alongside model names and evaluation date."
|
| 300 |
+
"</div>",
|
| 301 |
+
unsafe_allow_html=True,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# ββ Dimension breakdown βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 305 |
+
st.markdown("### Dimension Breakdown")
|
| 306 |
+
st.caption(
|
| 307 |
+
"Each dimension is scored 0β100 and weighted by its contribution to the overall KVS. "
|
| 308 |
+
"The sensitivity badge shows how much the score depends on the specific AI model used."
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
dims = [
|
| 312 |
+
("Knowledge Novelty", "novelty", 0.30),
|
| 313 |
+
("Retrieval Utility", "retrieval", 0.20),
|
| 314 |
+
("Generation Utility", "generation", 0.25),
|
| 315 |
+
("Attribution", "attribution", 0.15),
|
| 316 |
+
("Demand Utility", "demand", 0.10),
|
| 317 |
+
]
|
| 318 |
+
|
| 319 |
+
for label, key, weight in dims:
|
| 320 |
+
sc = dim_scores[key]
|
| 321 |
+
contrib = kvs_result["weighted_contributions"][key]
|
| 322 |
+
filled = round(sc / 100 * 20)
|
| 323 |
+
bar_str = "β" * filled + "β" * (20 - filled)
|
| 324 |
+
pct = int(weight * 100)
|
| 325 |
+
sens = DIMENSION_META[key]["model_sensitivity"]
|
| 326 |
+
sc_color = SENSITIVITY_COLOR[sens]
|
| 327 |
+
|
| 328 |
+
col1, col2 = st.columns([4, 1])
|
| 329 |
+
with col1:
|
| 330 |
+
st.markdown(
|
| 331 |
+
f"**{label}** "
|
| 332 |
+
f"<span class='sens-badge' style='background:{sc_color}22;color:{sc_color};"
|
| 333 |
+
f"border:1px solid {sc_color}55;'>sensitivity: {sens}</span> \n"
|
| 334 |
+
f"`{bar_str}` **{sc}/100** "
|
| 335 |
+
f"<span style='color:#888;font-size:13px;'> Γ{pct}% = {contrib} pts</span>",
|
| 336 |
+
unsafe_allow_html=True,
|
| 337 |
+
)
|
| 338 |
+
with col2:
|
| 339 |
+
st.metric(label="score", value=str(sc), label_visibility="collapsed")
|
| 340 |
+
|
| 341 |
+
# ββ Detailed analysis expanders βββββββββββββββββββββββββββββββββββββββββββ
|
| 342 |
+
st.markdown("### Detailed Analysis")
|
| 343 |
+
|
| 344 |
+
# Module A
|
| 345 |
+
dmeta = DIMENSION_META["novelty"]
|
| 346 |
+
with st.expander(f"Module A β Knowledge Novelty Β· {dim_scores['novelty']}/100"):
|
| 347 |
+
sens = dmeta["model_sensitivity"]
|
| 348 |
+
sc = SENSITIVITY_COLOR[sens]
|
| 349 |
+
st.markdown(
|
| 350 |
+
f"<span class='sens-badge' style='background:{sc}22;color:{sc};border:1px solid {sc}55;'>"
|
| 351 |
+
f"Model sensitivity: {sens}</span>",
|
| 352 |
+
unsafe_allow_html=True,
|
| 353 |
+
)
|
| 354 |
+
st.markdown(f"**What this measures:** {dmeta['description']}")
|
| 355 |
+
st.markdown(f"**How it's measured:** {dmeta['how_measured']}")
|
| 356 |
+
st.markdown(f"**Models used:** {', '.join(dmeta['models_used'])}")
|
| 357 |
+
st.info(dmeta["sensitivity_note"])
|
| 358 |
+
st.divider()
|
| 359 |
+
st.markdown(f"**Result:** {module_results['novelty']['summary']}")
|
| 360 |
+
details = module_results["novelty"].get("details", [])
|
| 361 |
+
if details:
|
| 362 |
+
st.markdown("**Claim analysis** (π’ novel Β· π‘ partial Β· π΄ already known):")
|
| 363 |
+
for d in details:
|
| 364 |
+
known_pct = round(d["known_score"] * 100)
|
| 365 |
+
icon = "π’" if d["known_score"] < 0.4 else ("π‘" if d["known_score"] < 0.7 else "π΄")
|
| 366 |
+
st.markdown(
|
| 367 |
+
f"{icon} **{d['claim'][:130]}** \n"
|
| 368 |
+
f"*Known to model: {known_pct}% β {d['reason']}*"
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
# Module B
|
| 372 |
+
dmeta = DIMENSION_META["retrieval"]
|
| 373 |
+
with st.expander(f"Module B β Retrieval Utility Β· {dim_scores['retrieval']}/100"):
|
| 374 |
+
sens = dmeta["model_sensitivity"]
|
| 375 |
+
sc = SENSITIVITY_COLOR[sens]
|
| 376 |
+
st.markdown(
|
| 377 |
+
f"<span class='sens-badge' style='background:{sc}22;color:{sc};border:1px solid {sc}55;'>"
|
| 378 |
+
f"Model sensitivity: {sens}</span>",
|
| 379 |
+
unsafe_allow_html=True,
|
| 380 |
+
)
|
| 381 |
+
st.markdown(f"**What this measures:** {dmeta['description']}")
|
| 382 |
+
st.markdown(f"**How it's measured:** {dmeta['how_measured']}")
|
| 383 |
+
st.markdown(f"**Models used:** {', '.join(dmeta['models_used'])}")
|
| 384 |
+
st.info(dmeta["sensitivity_note"])
|
| 385 |
+
st.divider()
|
| 386 |
+
st.markdown(f"**Result:** {module_results['retrieval']['summary']}")
|
| 387 |
+
details = module_results["retrieval"].get("details", [])
|
| 388 |
+
if details:
|
| 389 |
+
st.caption(
|
| 390 |
+
"**Recall@3** β fraction of queries where the correct chunk appears in top 3 results (1.0 = perfect). \n"
|
| 391 |
+
"**MRR** β Mean Reciprocal Rank; how high the correct chunk ranks on average (1.0 = always first)."
|
| 392 |
+
)
|
| 393 |
+
st.table({
|
| 394 |
+
"Query": [d["query"] for d in details],
|
| 395 |
+
"Recall@3": [f"{d['recall_at_3']:.2f}" for d in details],
|
| 396 |
+
"MRR": [f"{d['reciprocal_rank']:.2f}" for d in details],
|
| 397 |
+
})
|
| 398 |
+
|
| 399 |
+
# Module C
|
| 400 |
+
dmeta = DIMENSION_META["generation"]
|
| 401 |
+
with st.expander(f"Module C β Generation Utility Β· {dim_scores['generation']}/100"):
|
| 402 |
+
sens = dmeta["model_sensitivity"]
|
| 403 |
+
sc = SENSITIVITY_COLOR[sens]
|
| 404 |
+
st.markdown(
|
| 405 |
+
f"<span class='sens-badge' style='background:{sc}22;color:{sc};border:1px solid {sc}55;'>"
|
| 406 |
+
f"Model sensitivity: {sens}</span>",
|
| 407 |
+
unsafe_allow_html=True,
|
| 408 |
+
)
|
| 409 |
+
st.markdown(f"**What this measures:** {dmeta['description']}")
|
| 410 |
+
st.markdown(f"**How it's measured:** {dmeta['how_measured']}")
|
| 411 |
+
st.markdown(f"**Models used:** {', '.join(dmeta['models_used'])}")
|
| 412 |
+
st.info(dmeta["sensitivity_note"])
|
| 413 |
+
st.divider()
|
| 414 |
+
st.markdown(f"**Result:** {module_results['generation']['summary']}")
|
| 415 |
+
for d in module_results["generation"].get("details", []):
|
| 416 |
+
st.markdown(f"**Q: {d['question']}**")
|
| 417 |
+
c1, c2 = st.columns(2)
|
| 418 |
+
with c1:
|
| 419 |
+
st.markdown("*Baseline β no document:*")
|
| 420 |
+
st.markdown(f"> {d['baseline_answer'][:350]}")
|
| 421 |
+
with c2:
|
| 422 |
+
st.markdown("*RAG β with document:*")
|
| 423 |
+
st.markdown(f"> {d['rag_answer'][:350]}")
|
| 424 |
+
st.caption(
|
| 425 |
+
f"Improvement: **{d['improvement']}/100** | "
|
| 426 |
+
f"Accuracy: {d['accuracy']}/5 | "
|
| 427 |
+
f"Completeness: {d['completeness']}/5 | "
|
| 428 |
+
f"Specificity: {d['specificity']}/5 \n{d['reason']}"
|
| 429 |
+
)
|
| 430 |
+
st.divider()
|
| 431 |
+
|
| 432 |
+
# Module D
|
| 433 |
+
dmeta = DIMENSION_META["attribution"]
|
| 434 |
+
with st.expander(f"Module D β Attribution & Grounding Β· {dim_scores['attribution']}/100"):
|
| 435 |
+
sens = dmeta["model_sensitivity"]
|
| 436 |
+
sc = SENSITIVITY_COLOR[sens]
|
| 437 |
+
st.markdown(
|
| 438 |
+
f"<span class='sens-badge' style='background:{sc}22;color:{sc};border:1px solid {sc}55;'>"
|
| 439 |
+
f"Model sensitivity: {sens}</span>",
|
| 440 |
+
unsafe_allow_html=True,
|
| 441 |
+
)
|
| 442 |
+
st.markdown(f"**What this measures:** {dmeta['description']}")
|
| 443 |
+
st.markdown(f"**How it's measured:** {dmeta['how_measured']}")
|
| 444 |
+
st.markdown(f"**Models used:** {', '.join(dmeta['models_used'])}")
|
| 445 |
+
st.info(dmeta["sensitivity_note"])
|
| 446 |
+
st.divider()
|
| 447 |
+
st.markdown(f"**Result:** {module_results['attribution']['summary']}")
|
| 448 |
+
for d in module_results["attribution"].get("details", []):
|
| 449 |
+
halluc = "β οΈ Hallucination detected" if d.get("hallucination_detected") else "β No hallucination"
|
| 450 |
+
st.markdown(
|
| 451 |
+
f"**Q: {d['question'][:110]}** \n"
|
| 452 |
+
f"Grounding: **{round(d['grounding_fraction']*100)}%** | "
|
| 453 |
+
f"Semantic similarity: {d['semantic_similarity']} | {halluc}"
|
| 454 |
+
)
|
| 455 |
+
if d.get("ungrounded_claims"):
|
| 456 |
+
st.caption("Ungrounded claims: " + "; ".join(d["ungrounded_claims"][:3]))
|
| 457 |
+
if d.get("reason"):
|
| 458 |
+
st.caption(d["reason"])
|
| 459 |
+
|
| 460 |
+
# Module E
|
| 461 |
+
dmeta = DIMENSION_META["demand"]
|
| 462 |
+
with st.expander(f"Module E β Demand Utility Β· {dim_scores['demand']}/100"):
|
| 463 |
+
sens = dmeta["model_sensitivity"]
|
| 464 |
+
sc = SENSITIVITY_COLOR[sens]
|
| 465 |
+
st.markdown(
|
| 466 |
+
f"<span class='sens-badge' style='background:{sc}22;color:{sc};border:1px solid {sc}55;'>"
|
| 467 |
+
f"Model sensitivity: {sens}</span>",
|
| 468 |
+
unsafe_allow_html=True,
|
| 469 |
+
)
|
| 470 |
+
st.markdown(f"**What this measures:** {dmeta['description']}")
|
| 471 |
+
st.markdown(f"**How it's measured:** {dmeta['how_measured']}")
|
| 472 |
+
st.markdown(f"**Models used:** {', '.join(dmeta['models_used'])}")
|
| 473 |
+
st.info(dmeta["sensitivity_note"])
|
| 474 |
+
st.divider()
|
| 475 |
+
st.markdown(f"**Result:** {module_results['demand']['summary']}")
|
| 476 |
+
topics = module_results["demand"].get("topics", [])
|
| 477 |
+
if topics:
|
| 478 |
+
st.caption(
|
| 479 |
+
"**Query Freq** β estimated user query frequency for this topic (1 = rare, 10 = very common). \n"
|
| 480 |
+
"**Priority Domain** β whether this is a high-impact sector (health, climate, food, policy, etc.). \n"
|
| 481 |
+
"**Unmet Need** β whether existing AI models fall short in covering this topic."
|
| 482 |
+
)
|
| 483 |
+
st.table({
|
| 484 |
+
"Topic": [t.get("topic", "") for t in topics],
|
| 485 |
+
"Query Freq (1-10)": [t.get("query_frequency", "-") for t in topics],
|
| 486 |
+
"Priority Domain": ["Yes" if t.get("priority_domain") else "No" for t in topics],
|
| 487 |
+
"Unmet Need": ["Yes" if t.get("unmet_need") else "No" for t in topics],
|
| 488 |
+
"Rationale": [t.get("rationale", "")[:80] for t in topics],
|
| 489 |
+
})
|
| 490 |
+
|
| 491 |
+
# ββ Recommendations βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 492 |
+
st.markdown("### Recommended Actions")
|
| 493 |
+
for rec in kvs_result["recommendations"]:
|
| 494 |
+
st.markdown(f"- {rec}")
|
| 495 |
+
|
| 496 |
+
# ββ Download ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 497 |
+
st.divider()
|
| 498 |
+
report_md = report.generate(doc.title, kvs_result, module_results, meta)
|
| 499 |
+
st.download_button(
|
| 500 |
+
label="β¬ Download Full Report (Markdown)",
|
| 501 |
+
data=report_md,
|
| 502 |
+
file_name=f"kvl_report_{doc.title[:40].replace(' ', '_')}.md",
|
| 503 |
+
mime="text/markdown",
|
| 504 |
+
use_container_width=True,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
elif not uploaded:
|
| 508 |
+
st.info("Upload a `.md` file above to begin evaluation.")
|
kvl/__init__.py
ADDED
|
File without changes
|
kvl/config.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Centralized model configuration and dimension metadata for KVL."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
# ββ Model identifiers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
|
| 8 |
+
MODELS = {
|
| 9 |
+
"judge": {
|
| 10 |
+
"id": "claude-sonnet-4-6",
|
| 11 |
+
"display": "Claude Sonnet 4.6",
|
| 12 |
+
"role": "Evaluation judge β novelty scoring, grounding assessment, demand analysis",
|
| 13 |
+
"modules": ["A", "D", "E"],
|
| 14 |
+
},
|
| 15 |
+
"worker": {
|
| 16 |
+
"id": "claude-haiku-4-5-20251001",
|
| 17 |
+
"display": "Claude Haiku 4.5",
|
| 18 |
+
"role": "Closed-book QA, baseline & RAG answer generation, query generation",
|
| 19 |
+
"modules": ["A", "B", "C"],
|
| 20 |
+
},
|
| 21 |
+
"embedder": {
|
| 22 |
+
"id": "sentence-transformers/all-MiniLM-L6-v2",
|
| 23 |
+
"display": "all-MiniLM-L6-v2",
|
| 24 |
+
"role": "Text embeddings for retrieval index and semantic similarity",
|
| 25 |
+
"modules": ["B", "D"],
|
| 26 |
+
},
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def model_meta(eval_date: str | None = None) -> dict:
|
| 31 |
+
return {
|
| 32 |
+
"judge": MODELS["judge"],
|
| 33 |
+
"worker": MODELS["worker"],
|
| 34 |
+
"embedder": MODELS["embedder"],
|
| 35 |
+
"eval_date": eval_date or datetime.now().strftime("%Y-%m-%d %H:%M UTC"),
|
| 36 |
+
"framework_version": "KVL v0.1",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ββ Per-dimension metadata ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
+
|
| 42 |
+
DIMENSION_META = {
|
| 43 |
+
"novelty": {
|
| 44 |
+
"name": "Knowledge Novelty",
|
| 45 |
+
"weight": 0.30,
|
| 46 |
+
"model_sensitivity": "High",
|
| 47 |
+
"sensitivity_note": (
|
| 48 |
+
"Directly tied to the specific model evaluated. A document scoring 96/100 "
|
| 49 |
+
"against Claude Sonnet may score significantly lower against a model trained "
|
| 50 |
+
"on domain-specific corpora. Re-evaluate when the underlying model is updated."
|
| 51 |
+
),
|
| 52 |
+
"description": (
|
| 53 |
+
"Measures how much of the document's content is already embedded in the AI "
|
| 54 |
+
"model's pre-training. A high novelty score means the model cannot reliably "
|
| 55 |
+
"answer questions about this document from its training alone β the document "
|
| 56 |
+
"contributes genuinely new knowledge to the system."
|
| 57 |
+
),
|
| 58 |
+
"how_measured": (
|
| 59 |
+
"12β15 specific factual claims are extracted from the document. For each claim, "
|
| 60 |
+
"a question is generated and posed to the model **without** the document "
|
| 61 |
+
"(closed-book). A judge model scores how well the model's answer matches the "
|
| 62 |
+
"document's claim. **Novelty = 100 Γ (1 β fraction already known)**."
|
| 63 |
+
),
|
| 64 |
+
"high_means": "Document contains knowledge rare or absent in the model's training data.",
|
| 65 |
+
"low_means": "Document content is widely represented in the model's training corpus.",
|
| 66 |
+
"models_used": ["Claude Haiku 4.5 (closed-book QA)", "Claude Sonnet 4.6 (judge)"],
|
| 67 |
+
},
|
| 68 |
+
"retrieval": {
|
| 69 |
+
"name": "Retrieval Utility",
|
| 70 |
+
"weight": 0.20,
|
| 71 |
+
"model_sensitivity": "Low",
|
| 72 |
+
"sensitivity_note": (
|
| 73 |
+
"The most model-independent dimension. Embeddings use a fixed local model "
|
| 74 |
+
"(all-MiniLM-L6-v2); only test-query generation involves the LLM. "
|
| 75 |
+
"Scores are stable across LLM versions."
|
| 76 |
+
),
|
| 77 |
+
"description": (
|
| 78 |
+
"Measures how effectively the document surfaces in response to relevant user "
|
| 79 |
+
"queries in a retrieval-augmented system. High retrieval utility means the "
|
| 80 |
+
"document's content consistently appears in top search results when users "
|
| 81 |
+
"ask about its topics."
|
| 82 |
+
),
|
| 83 |
+
"how_measured": (
|
| 84 |
+
"The document is split into ~400-word chunks and embedded using "
|
| 85 |
+
"sentence-transformers. 8 representative queries are generated from the document, "
|
| 86 |
+
"and for each query the system measures whether the most relevant chunk appears "
|
| 87 |
+
"in the top-3 results. **Score = weighted Recall@3 (60%) + MRR (40%)**."
|
| 88 |
+
),
|
| 89 |
+
"high_means": "Well-structured content that maps cleanly to user search queries.",
|
| 90 |
+
"low_means": "Poor chunk boundaries, dense prose, or missing structure β can be improved through document restructuring.",
|
| 91 |
+
"models_used": ["all-MiniLM-L6-v2 (embeddings)", "Claude Haiku 4.5 (query generation)"],
|
| 92 |
+
},
|
| 93 |
+
"generation": {
|
| 94 |
+
"name": "Generation Utility",
|
| 95 |
+
"weight": 0.25,
|
| 96 |
+
"model_sensitivity": "High",
|
| 97 |
+
"sensitivity_note": (
|
| 98 |
+
"Highly model-relative. A model with stronger prior knowledge will show less "
|
| 99 |
+
"improvement from the document, yielding a lower generation utility score "
|
| 100 |
+
"even if the document is objectively high quality. Re-evaluate when the model changes."
|
| 101 |
+
),
|
| 102 |
+
"description": (
|
| 103 |
+
"Measures the uplift in AI-generated answer quality when the document is "
|
| 104 |
+
"provided as context (RAG) versus answering from the model's prior knowledge "
|
| 105 |
+
"alone. This directly quantifies how much the document improves what the "
|
| 106 |
+
"AI system can tell users."
|
| 107 |
+
),
|
| 108 |
+
"how_measured": (
|
| 109 |
+
"8 questions are derived from the document. Each is answered twice: once "
|
| 110 |
+
"**without** the document (baseline) and once **with** the document as "
|
| 111 |
+
"context (RAG). A judge model scores the improvement across accuracy, "
|
| 112 |
+
"completeness, and specificity on a 0β100 scale."
|
| 113 |
+
),
|
| 114 |
+
"high_means": "Document substantially improves AI answers β essential for advisory and QA applications.",
|
| 115 |
+
"low_means": "Model can already answer adequately without the document.",
|
| 116 |
+
"models_used": ["Claude Haiku 4.5 (answer generation)", "Claude Sonnet 4.6 (judge)"],
|
| 117 |
+
},
|
| 118 |
+
"attribution": {
|
| 119 |
+
"name": "Attribution & Grounding",
|
| 120 |
+
"weight": 0.15,
|
| 121 |
+
"model_sensitivity": "Moderate",
|
| 122 |
+
"sensitivity_note": (
|
| 123 |
+
"The LLM judge's grounding assessment varies across models. The semantic "
|
| 124 |
+
"similarity component (sentence-transformers) is model-independent, providing "
|
| 125 |
+
"a stable floor for the score."
|
| 126 |
+
),
|
| 127 |
+
"description": (
|
| 128 |
+
"Measures whether AI-generated answers are genuinely grounded in the document, "
|
| 129 |
+
"rather than mixing in unverified claims or hallucinations. High grounding means "
|
| 130 |
+
"the document's evidence is being faithfully used and answers are trustworthy "
|
| 131 |
+
"for deployment in high-stakes domains."
|
| 132 |
+
),
|
| 133 |
+
"how_measured": (
|
| 134 |
+
"RAG answers from the Generation module are analysed by a judge model to "
|
| 135 |
+
"identify which claims are traceable to specific document passages. Semantic "
|
| 136 |
+
"similarity between the answer and document context provides a second signal. "
|
| 137 |
+
"A score penalty is applied if hallucination is detected. "
|
| 138 |
+
"**Score = 70% Γ LLM grounding fraction + 30% Γ semantic similarity β hallucination penalty**."
|
| 139 |
+
),
|
| 140 |
+
"high_means": "Answers closely follow document β high trustworthiness for regulated or high-stakes deployment.",
|
| 141 |
+
"low_means": "Answers stray from document content β document may lack specificity or model over-relies on prior knowledge.",
|
| 142 |
+
"models_used": ["Claude Sonnet 4.6 (grounding judge)", "all-MiniLM-L6-v2 (semantic similarity)"],
|
| 143 |
+
},
|
| 144 |
+
"demand": {
|
| 145 |
+
"name": "Demand Utility",
|
| 146 |
+
"weight": 0.10,
|
| 147 |
+
"model_sensitivity": "Low",
|
| 148 |
+
"sensitivity_note": (
|
| 149 |
+
"Primarily reflects the document's subject matter rather than the model's "
|
| 150 |
+
"knowledge state. Scores are relatively stable across LLM versions, though "
|
| 151 |
+
"different models may assess domain importance differently."
|
| 152 |
+
),
|
| 153 |
+
"description": (
|
| 154 |
+
"Estimates how frequently the knowledge in this document would be needed by "
|
| 155 |
+
"real users β considering topic popularity, domain priority (healthcare, climate, "
|
| 156 |
+
"agriculture, policy), geographic relevance, and coverage of unmet information needs."
|
| 157 |
+
),
|
| 158 |
+
"how_measured": (
|
| 159 |
+
"5β8 topics are extracted from the document. For each, the model estimates "
|
| 160 |
+
"query frequency (1β10), whether the domain is a priority area, and whether "
|
| 161 |
+
"significant unmet information needs exist. A holistic demand score from the "
|
| 162 |
+
"judge is blended with the topic-level estimates "
|
| 163 |
+
"(40% topic average + 60% holistic score)."
|
| 164 |
+
),
|
| 165 |
+
"high_means": "Broad audience or urgent unmet need β wide deployment and open access recommended.",
|
| 166 |
+
"low_means": "Narrow or specialised audience β targeted distribution channels are more appropriate.",
|
| 167 |
+
"models_used": ["Claude Haiku 4.5 (topic extraction)", "Claude Sonnet 4.6 (demand assessment)"],
|
| 168 |
+
},
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
SENSITIVITY_COLOR = {
|
| 173 |
+
"High": "#ef476f",
|
| 174 |
+
"Moderate": "#f8961e",
|
| 175 |
+
"Low": "#06d6a0",
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
KVS_CLASSIFICATION = [
|
| 179 |
+
(81, "Transformational Value", "Document adds exceptional, rare knowledge and dramatically improves AI system performance."),
|
| 180 |
+
(61, "High Value", "Document adds significant novel knowledge with strong retrieval and generation impact."),
|
| 181 |
+
(41, "Moderate Value", "Document offers meaningful contributions in at least some dimensions."),
|
| 182 |
+
(21, "Incremental Value", "Document provides marginal improvements; content may be partially redundant."),
|
| 183 |
+
(0, "Minimal Value", "Little or no measurable improvement to AI systems from this document."),
|
| 184 |
+
]
|
kvl/ingestor.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parse and chunk Markdown documents for KVL evaluation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import re
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class Chunk:
|
| 10 |
+
text: str
|
| 11 |
+
index: int
|
| 12 |
+
section: str = ""
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class Document:
|
| 17 |
+
raw: str
|
| 18 |
+
title: str
|
| 19 |
+
sections: list[dict] # [{"heading": str, "body": str}]
|
| 20 |
+
chunks: list[Chunk]
|
| 21 |
+
word_count: int
|
| 22 |
+
char_count: int
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _extract_title(text: str) -> str:
|
| 26 |
+
for line in text.splitlines():
|
| 27 |
+
line = line.strip()
|
| 28 |
+
if line.startswith("# "):
|
| 29 |
+
return line[2:].strip()
|
| 30 |
+
return "Untitled Document"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _split_sections(text: str) -> list[dict]:
|
| 34 |
+
"""Split on any heading level, preserving body text."""
|
| 35 |
+
pattern = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
|
| 36 |
+
matches = list(pattern.finditer(text))
|
| 37 |
+
sections = []
|
| 38 |
+
for i, m in enumerate(matches):
|
| 39 |
+
heading = m.group(2).strip()
|
| 40 |
+
start = m.end()
|
| 41 |
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
| 42 |
+
body = text[start:end].strip()
|
| 43 |
+
if body:
|
| 44 |
+
sections.append({"heading": heading, "body": body})
|
| 45 |
+
if not sections:
|
| 46 |
+
sections.append({"heading": "Content", "body": text.strip()})
|
| 47 |
+
return sections
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _chunk_text(sections: list[dict], chunk_size: int = 400, overlap: int = 50) -> list[Chunk]:
|
| 51 |
+
"""Sliding-window word-based chunking over section bodies."""
|
| 52 |
+
chunks: list[Chunk] = []
|
| 53 |
+
idx = 0
|
| 54 |
+
for sec in sections:
|
| 55 |
+
words = sec["body"].split()
|
| 56 |
+
pos = 0
|
| 57 |
+
while pos < len(words):
|
| 58 |
+
window = words[pos: pos + chunk_size]
|
| 59 |
+
text = " ".join(window)
|
| 60 |
+
chunks.append(Chunk(text=text, index=idx, section=sec["heading"]))
|
| 61 |
+
idx += 1
|
| 62 |
+
if pos + chunk_size >= len(words):
|
| 63 |
+
break
|
| 64 |
+
pos += chunk_size - overlap
|
| 65 |
+
return chunks
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def parse(md_text: str) -> Document:
|
| 69 |
+
text = md_text.strip()
|
| 70 |
+
title = _extract_title(text)
|
| 71 |
+
sections = _split_sections(text)
|
| 72 |
+
chunks = _chunk_text(sections)
|
| 73 |
+
return Document(
|
| 74 |
+
raw=text,
|
| 75 |
+
title=title,
|
| 76 |
+
sections=sections,
|
| 77 |
+
chunks=chunks,
|
| 78 |
+
word_count=len(text.split()),
|
| 79 |
+
char_count=len(text),
|
| 80 |
+
)
|
kvl/modules/__init__.py
ADDED
|
File without changes
|
kvl/modules/attribution.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module D: Attribution & Grounding β verifies that RAG answers are grounded in the document."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import json
|
| 5 |
+
import numpy as np
|
| 6 |
+
import anthropic
|
| 7 |
+
from kvl.ingestor import Document
|
| 8 |
+
|
| 9 |
+
_GROUNDING_PROMPT = """Analyze whether an AI-generated answer is properly grounded in the provided source document.
|
| 10 |
+
|
| 11 |
+
Source document excerpt:
|
| 12 |
+
{context}
|
| 13 |
+
|
| 14 |
+
AI-generated answer:
|
| 15 |
+
{answer}
|
| 16 |
+
|
| 17 |
+
Evaluate grounding quality:
|
| 18 |
+
1. What fraction of specific claims in the answer can be traced to the source document? (0.0-1.0)
|
| 19 |
+
2. Does the answer avoid hallucinating facts not in the document?
|
| 20 |
+
3. Are the answer's assertions supported by evidence in the document?
|
| 21 |
+
|
| 22 |
+
Return ONLY JSON:
|
| 23 |
+
{{
|
| 24 |
+
"grounding_fraction": <float 0-1>,
|
| 25 |
+
"hallucination_detected": <bool>,
|
| 26 |
+
"grounded_claims": ["list of claims that are in the document"],
|
| 27 |
+
"ungrounded_claims": ["list of claims NOT found in the document"],
|
| 28 |
+
"reason": "one sentence summary"
|
| 29 |
+
}}"""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _call_claude(client: anthropic.Anthropic, prompt: str) -> str:
|
| 33 |
+
msg = client.messages.create(
|
| 34 |
+
model="claude-sonnet-4-6",
|
| 35 |
+
max_tokens=1024,
|
| 36 |
+
messages=[{"role": "user", "content": prompt}],
|
| 37 |
+
system="You are an expert fact-checker assessing source attribution in AI-generated text.",
|
| 38 |
+
)
|
| 39 |
+
return msg.content[0].text.strip()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _semantic_overlap(answer: str, context: str, embedder) -> float:
|
| 43 |
+
"""Cosine similarity between answer and document context as a grounding signal."""
|
| 44 |
+
embs = embedder.encode([answer, context], normalize_embeddings=True, show_progress_bar=False)
|
| 45 |
+
return float(np.dot(embs[0], embs[1]))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def evaluate(client: anthropic.Anthropic, doc: Document, generation_results: dict, embedder, progress_cb=None) -> dict:
|
| 49 |
+
"""Return grounding score (0-100) using outputs from the generation module."""
|
| 50 |
+
details_list = generation_results.get("details", [])
|
| 51 |
+
if not details_list:
|
| 52 |
+
return {"score": 50, "details": [], "summary": "No generation results to assess grounding."}
|
| 53 |
+
|
| 54 |
+
context = " ".join(doc.raw.split()[:4000])
|
| 55 |
+
results = []
|
| 56 |
+
grounding_scores = []
|
| 57 |
+
|
| 58 |
+
for i, item in enumerate(details_list):
|
| 59 |
+
if progress_cb:
|
| 60 |
+
progress_cb(f"Checking grounding for answer {i+1}/{len(details_list)}...")
|
| 61 |
+
|
| 62 |
+
rag_answer = item.get("rag_answer", "")
|
| 63 |
+
if not rag_answer:
|
| 64 |
+
continue
|
| 65 |
+
|
| 66 |
+
raw = _call_claude(client, _GROUNDING_PROMPT.format(context=context, answer=rag_answer))
|
| 67 |
+
raw = raw.strip()
|
| 68 |
+
if raw.startswith("```"):
|
| 69 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 70 |
+
raw = raw.rsplit("```", 1)[0]
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
judgment = json.loads(raw)
|
| 74 |
+
except json.JSONDecodeError:
|
| 75 |
+
judgment = {"grounding_fraction": 0.5, "hallucination_detected": False, "reason": "Parse error."}
|
| 76 |
+
|
| 77 |
+
# Combine LLM grounding fraction with semantic overlap
|
| 78 |
+
llm_grounding = judgment.get("grounding_fraction", 0.5)
|
| 79 |
+
semantic_sim = _semantic_overlap(rag_answer, context, embedder)
|
| 80 |
+
# Penalise if hallucination detected
|
| 81 |
+
hallucination_penalty = 0.2 if judgment.get("hallucination_detected", False) else 0.0
|
| 82 |
+
combined = (0.7 * llm_grounding + 0.3 * semantic_sim) - hallucination_penalty
|
| 83 |
+
combined = max(0.0, min(1.0, combined))
|
| 84 |
+
|
| 85 |
+
grounding_scores.append(combined)
|
| 86 |
+
results.append({
|
| 87 |
+
"question": item.get("question", ""),
|
| 88 |
+
"answer": rag_answer,
|
| 89 |
+
"grounding_fraction": llm_grounding,
|
| 90 |
+
"semantic_similarity": round(semantic_sim, 3),
|
| 91 |
+
"hallucination_detected": judgment.get("hallucination_detected", False),
|
| 92 |
+
"grounded_claims": judgment.get("grounded_claims", []),
|
| 93 |
+
"ungrounded_claims": judgment.get("ungrounded_claims", []),
|
| 94 |
+
"reason": judgment.get("reason", ""),
|
| 95 |
+
"combined_score": round(combined, 3),
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
if not grounding_scores:
|
| 99 |
+
return {"score": 50, "details": results, "summary": "No grounding assessments completed."}
|
| 100 |
+
|
| 101 |
+
avg_grounding = sum(grounding_scores) / len(grounding_scores)
|
| 102 |
+
score = round(avg_grounding * 100)
|
| 103 |
+
|
| 104 |
+
hallucinations = sum(1 for r in results if r.get("hallucination_detected"))
|
| 105 |
+
return {
|
| 106 |
+
"score": score,
|
| 107 |
+
"details": results,
|
| 108 |
+
"summary": f"Average grounding: {score}/100. Hallucinations detected in {hallucinations}/{len(results)} answers.",
|
| 109 |
+
}
|
kvl/modules/demand.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module E: Demand Utility β estimates how frequently this knowledge is needed by users."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import json
|
| 5 |
+
import anthropic
|
| 6 |
+
from kvl.ingestor import Document
|
| 7 |
+
|
| 8 |
+
_TOPIC_PROMPT = """Analyze this document and identify its main topics and themes.
|
| 9 |
+
|
| 10 |
+
For each topic, estimate:
|
| 11 |
+
1. How frequently real users would search for or need this information (1-10 scale)
|
| 12 |
+
2. Whether this is a high-priority domain (healthcare, climate, food security, education, policy, etc.)
|
| 13 |
+
3. Whether there are significant unmet information needs in this area
|
| 14 |
+
|
| 15 |
+
Return ONLY a JSON array of objects:
|
| 16 |
+
[
|
| 17 |
+
{{
|
| 18 |
+
"topic": "topic name",
|
| 19 |
+
"description": "what aspect of the document this covers",
|
| 20 |
+
"query_frequency": <int 1-10>,
|
| 21 |
+
"priority_domain": <bool>,
|
| 22 |
+
"unmet_need": <bool>,
|
| 23 |
+
"rationale": "one sentence"
|
| 24 |
+
}},
|
| 25 |
+
...
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
Extract 5-8 topics.
|
| 29 |
+
|
| 30 |
+
Document:
|
| 31 |
+
{document}"""
|
| 32 |
+
|
| 33 |
+
_DEMAND_SUMMARY_PROMPT = """Based on this knowledge document's topics and characteristics, assess its overall demand utility.
|
| 34 |
+
|
| 35 |
+
Topics identified:
|
| 36 |
+
{topics}
|
| 37 |
+
|
| 38 |
+
Document summary:
|
| 39 |
+
{summary}
|
| 40 |
+
|
| 41 |
+
Evaluate:
|
| 42 |
+
1. How broad vs. specialized is the audience for this knowledge?
|
| 43 |
+
2. Are there gaps in existing AI model knowledge that this document fills?
|
| 44 |
+
3. What is the geographic or linguistic scope?
|
| 45 |
+
4. How actionable is this knowledge for typical users?
|
| 46 |
+
|
| 47 |
+
Return ONLY JSON:
|
| 48 |
+
{{
|
| 49 |
+
"audience_breadth": <int 1-10>,
|
| 50 |
+
"knowledge_gap_fill": <int 1-10>,
|
| 51 |
+
"geographic_relevance": <int 1-10>,
|
| 52 |
+
"actionability": <int 1-10>,
|
| 53 |
+
"demand_score": <int 0-100>,
|
| 54 |
+
"summary": "two sentence demand assessment"
|
| 55 |
+
}}"""
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _call_claude(client: anthropic.Anthropic, prompt: str, model: str = "claude-sonnet-4-6") -> str:
|
| 59 |
+
msg = client.messages.create(
|
| 60 |
+
model=model,
|
| 61 |
+
max_tokens=1024,
|
| 62 |
+
messages=[{"role": "user", "content": prompt}],
|
| 63 |
+
system="You are an expert in knowledge management and information demand analysis.",
|
| 64 |
+
)
|
| 65 |
+
return msg.content[0].text.strip()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _parse_json(raw: str) -> dict | list | None:
|
| 69 |
+
raw = raw.strip()
|
| 70 |
+
if raw.startswith("```"):
|
| 71 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 72 |
+
raw = raw.rsplit("```", 1)[0]
|
| 73 |
+
try:
|
| 74 |
+
return json.loads(raw)
|
| 75 |
+
except json.JSONDecodeError:
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def evaluate(client: anthropic.Anthropic, doc: Document, progress_cb=None) -> dict:
|
| 80 |
+
"""Return demand utility score (0-100) and topic analysis."""
|
| 81 |
+
if progress_cb:
|
| 82 |
+
progress_cb("Extracting document topics and themes...")
|
| 83 |
+
|
| 84 |
+
text = " ".join(doc.raw.split()[:5000])
|
| 85 |
+
raw_topics = _call_claude(client, _TOPIC_PROMPT.format(document=text), model="claude-haiku-4-5-20251001")
|
| 86 |
+
topics = _parse_json(raw_topics)
|
| 87 |
+
|
| 88 |
+
if not topics or not isinstance(topics, list):
|
| 89 |
+
return {"score": 50, "topics": [], "summary": "Could not extract topics from document."}
|
| 90 |
+
|
| 91 |
+
# Compute a base score from topic frequency ratings
|
| 92 |
+
avg_frequency = sum(t.get("query_frequency", 5) for t in topics) / len(topics)
|
| 93 |
+
priority_bonus = sum(1 for t in topics if t.get("priority_domain", False)) / len(topics)
|
| 94 |
+
unmet_bonus = sum(1 for t in topics if t.get("unmet_need", False)) / len(topics)
|
| 95 |
+
|
| 96 |
+
# Base score from topic analysis
|
| 97 |
+
base_score = (avg_frequency / 10) * 60 + priority_bonus * 20 + unmet_bonus * 20
|
| 98 |
+
|
| 99 |
+
if progress_cb:
|
| 100 |
+
progress_cb("Estimating overall demand utility...")
|
| 101 |
+
|
| 102 |
+
# Get holistic demand assessment from Claude
|
| 103 |
+
doc_summary = f"Title: {doc.title}. Words: {doc.word_count}. Sections: {len(doc.sections)}."
|
| 104 |
+
topics_str = json.dumps(topics, indent=2)
|
| 105 |
+
raw_demand = _call_claude(client, _DEMAND_SUMMARY_PROMPT.format(topics=topics_str, summary=doc_summary))
|
| 106 |
+
demand_assessment = _parse_json(raw_demand)
|
| 107 |
+
|
| 108 |
+
if demand_assessment and isinstance(demand_assessment, dict):
|
| 109 |
+
llm_score = demand_assessment.get("demand_score", base_score)
|
| 110 |
+
# Blend base score (40%) with LLM holistic score (60%)
|
| 111 |
+
final_score = round(0.4 * base_score + 0.6 * llm_score)
|
| 112 |
+
assessment_summary = demand_assessment.get("summary", "")
|
| 113 |
+
else:
|
| 114 |
+
final_score = round(base_score)
|
| 115 |
+
assessment_summary = ""
|
| 116 |
+
|
| 117 |
+
final_score = max(0, min(100, final_score))
|
| 118 |
+
|
| 119 |
+
return {
|
| 120 |
+
"score": final_score,
|
| 121 |
+
"topics": topics,
|
| 122 |
+
"assessment": demand_assessment or {},
|
| 123 |
+
"summary": assessment_summary or f"Demand estimated from {len(topics)} topic areas. Base score: {round(base_score)}/100.",
|
| 124 |
+
}
|
kvl/modules/generation.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module C: Generation Utility β measures improvement in AI responses when using the document."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import json
|
| 5 |
+
import anthropic
|
| 6 |
+
from kvl.ingestor import Document
|
| 7 |
+
|
| 8 |
+
_QA_GEN_PROMPT = """Generate {n} specific questions whose answers can ONLY be found in this document (not general knowledge).
|
| 9 |
+
Focus on questions about specific findings, methods, results, or recommendations in the document.
|
| 10 |
+
|
| 11 |
+
Return ONLY a JSON array of question strings.
|
| 12 |
+
|
| 13 |
+
Document:
|
| 14 |
+
{document}"""
|
| 15 |
+
|
| 16 |
+
_JUDGE_PROMPT = """Compare two AI-generated answers to the same question. One is generated without any document context (baseline), the other with a relevant document as context (RAG).
|
| 17 |
+
|
| 18 |
+
Question: {question}
|
| 19 |
+
|
| 20 |
+
Baseline answer (no document): {baseline}
|
| 21 |
+
|
| 22 |
+
RAG answer (with document context): {rag_answer}
|
| 23 |
+
|
| 24 |
+
Rate the RAG answer improvement over baseline on these criteria (1-5 each):
|
| 25 |
+
- accuracy: Is the RAG answer more factually correct?
|
| 26 |
+
- completeness: Does the RAG answer cover more relevant aspects?
|
| 27 |
+
- specificity: Is the RAG answer more specific and precise?
|
| 28 |
+
|
| 29 |
+
Also provide an overall improvement score (0-100) where:
|
| 30 |
+
- 0 = RAG answer is no better or worse than baseline
|
| 31 |
+
- 50 = moderate improvement
|
| 32 |
+
- 100 = dramatically better, document was essential
|
| 33 |
+
|
| 34 |
+
Return ONLY JSON: {{"accuracy": int, "completeness": int, "specificity": int, "improvement": int, "reason": "one sentence"}}"""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _call_claude(client: anthropic.Anthropic, system: str, user: str, model: str = "claude-sonnet-4-6") -> str:
|
| 38 |
+
msg = client.messages.create(
|
| 39 |
+
model=model,
|
| 40 |
+
max_tokens=1024,
|
| 41 |
+
messages=[{"role": "user", "content": user}],
|
| 42 |
+
system=system,
|
| 43 |
+
)
|
| 44 |
+
return msg.content[0].text.strip()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _generate_questions(client: anthropic.Anthropic, doc: Document, n: int = 8) -> list[str]:
|
| 48 |
+
text = " ".join(doc.raw.split()[:5000])
|
| 49 |
+
raw = _call_claude(
|
| 50 |
+
client,
|
| 51 |
+
"You generate evaluation questions for knowledge documents.",
|
| 52 |
+
_QA_GEN_PROMPT.format(n=n, document=text),
|
| 53 |
+
model="claude-haiku-4-5-20251001",
|
| 54 |
+
)
|
| 55 |
+
raw = raw.strip()
|
| 56 |
+
if raw.startswith("```"):
|
| 57 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 58 |
+
raw = raw.rsplit("```", 1)[0]
|
| 59 |
+
try:
|
| 60 |
+
questions = json.loads(raw)
|
| 61 |
+
return [q for q in questions if isinstance(q, str)][:n]
|
| 62 |
+
except json.JSONDecodeError:
|
| 63 |
+
return []
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _baseline_answer(client: anthropic.Anthropic, question: str) -> str:
|
| 67 |
+
return _call_claude(
|
| 68 |
+
client,
|
| 69 |
+
"Answer based only on your pre-trained knowledge. Be honest if you don't know.",
|
| 70 |
+
question,
|
| 71 |
+
model="claude-haiku-4-5-20251001",
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _rag_answer(client: anthropic.Anthropic, question: str, context: str) -> str:
|
| 76 |
+
return _call_claude(
|
| 77 |
+
client,
|
| 78 |
+
"Answer the question using the provided document context. Be specific and cite details from the document.",
|
| 79 |
+
f"Document context:\n{context}\n\nQuestion: {question}",
|
| 80 |
+
model="claude-haiku-4-5-20251001",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _judge(client: anthropic.Anthropic, question: str, baseline: str, rag: str) -> dict:
|
| 85 |
+
raw = _call_claude(
|
| 86 |
+
client,
|
| 87 |
+
"You are an expert evaluator comparing AI answer quality.",
|
| 88 |
+
_JUDGE_PROMPT.format(question=question, baseline=baseline, rag_answer=rag),
|
| 89 |
+
)
|
| 90 |
+
raw = raw.strip()
|
| 91 |
+
if raw.startswith("```"):
|
| 92 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 93 |
+
raw = raw.rsplit("```", 1)[0]
|
| 94 |
+
try:
|
| 95 |
+
return json.loads(raw)
|
| 96 |
+
except json.JSONDecodeError:
|
| 97 |
+
return {"accuracy": 3, "completeness": 3, "specificity": 3, "improvement": 50, "reason": "Parse error."}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def evaluate(client: anthropic.Anthropic, doc: Document, progress_cb=None) -> dict:
|
| 101 |
+
"""Return generation utility score (0-100) and per-question details."""
|
| 102 |
+
if progress_cb:
|
| 103 |
+
progress_cb("Generating evaluation questions...")
|
| 104 |
+
|
| 105 |
+
questions = _generate_questions(client, doc)
|
| 106 |
+
if not questions:
|
| 107 |
+
return {"score": 50, "details": [], "summary": "Could not generate test questions."}
|
| 108 |
+
|
| 109 |
+
# Use the full document as RAG context (capped for token limits)
|
| 110 |
+
context = " ".join(doc.raw.split()[:4000])
|
| 111 |
+
|
| 112 |
+
results = []
|
| 113 |
+
improvement_scores = []
|
| 114 |
+
|
| 115 |
+
for i, question in enumerate(questions):
|
| 116 |
+
if progress_cb:
|
| 117 |
+
progress_cb(f"Evaluating question {i+1}/{len(questions)}: baseline vs RAG...")
|
| 118 |
+
|
| 119 |
+
baseline = _baseline_answer(client, question)
|
| 120 |
+
rag = _rag_answer(client, question, context)
|
| 121 |
+
judgment = _judge(client, question, baseline, rag)
|
| 122 |
+
|
| 123 |
+
improvement = judgment.get("improvement", 50)
|
| 124 |
+
improvement_scores.append(improvement)
|
| 125 |
+
results.append({
|
| 126 |
+
"question": question,
|
| 127 |
+
"baseline_answer": baseline,
|
| 128 |
+
"rag_answer": rag,
|
| 129 |
+
"accuracy": judgment.get("accuracy", 3),
|
| 130 |
+
"completeness": judgment.get("completeness", 3),
|
| 131 |
+
"specificity": judgment.get("specificity", 3),
|
| 132 |
+
"improvement": improvement,
|
| 133 |
+
"reason": judgment.get("reason", ""),
|
| 134 |
+
})
|
| 135 |
+
|
| 136 |
+
avg_improvement = sum(improvement_scores) / len(improvement_scores)
|
| 137 |
+
score = round(avg_improvement)
|
| 138 |
+
|
| 139 |
+
return {
|
| 140 |
+
"score": score,
|
| 141 |
+
"details": results,
|
| 142 |
+
"summary": f"Average RAG improvement: {score}/100 across {len(questions)} questions.",
|
| 143 |
+
}
|
kvl/modules/novelty.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module A: Knowledge Novelty β measures how much of the document Claude already knows."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import json
|
| 5 |
+
import anthropic
|
| 6 |
+
from kvl.ingestor import Document
|
| 7 |
+
|
| 8 |
+
_CLAIM_PROMPT = """You are analyzing a document to evaluate how novel its knowledge is to AI language models.
|
| 9 |
+
|
| 10 |
+
Extract exactly {n} specific, verifiable factual claims from the document below. Focus on:
|
| 11 |
+
- Precise facts, findings, statistics, or assertions
|
| 12 |
+
- Claims that are specific (not general background knowledge)
|
| 13 |
+
- Claims that could be tested with a yes/no or short answer question
|
| 14 |
+
|
| 15 |
+
Return ONLY a JSON array of objects with keys "claim" and "question", where "question" is the specific question whose answer is the claim.
|
| 16 |
+
|
| 17 |
+
Example format:
|
| 18 |
+
[
|
| 19 |
+
{{"claim": "The study found a 34% reduction in soil erosion.", "question": "What reduction in soil erosion did the study find?"}},
|
| 20 |
+
...
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
Document:
|
| 24 |
+
{document}"""
|
| 25 |
+
|
| 26 |
+
_JUDGE_PROMPT = """You are evaluating whether an AI model's answer demonstrates prior knowledge of a specific claim.
|
| 27 |
+
|
| 28 |
+
Claim from document: {claim}
|
| 29 |
+
AI model's closed-book answer: {answer}
|
| 30 |
+
|
| 31 |
+
Rate how well the model's answer already captures the claim WITHOUT having seen the document.
|
| 32 |
+
Score 0-1 where:
|
| 33 |
+
- 1.0 = model's answer is accurate and complete β this is not novel knowledge
|
| 34 |
+
- 0.5 = model has partial or approximate knowledge
|
| 35 |
+
- 0.0 = model doesn't know this β this IS novel knowledge
|
| 36 |
+
|
| 37 |
+
Return ONLY a JSON object: {{"score": <float 0-1>, "reason": "<one sentence>"}}"""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _call_claude(client: anthropic.Anthropic, system: str, user: str, model: str = "claude-sonnet-4-6") -> str:
|
| 41 |
+
msg = client.messages.create(
|
| 42 |
+
model=model,
|
| 43 |
+
max_tokens=2048,
|
| 44 |
+
messages=[{"role": "user", "content": user}],
|
| 45 |
+
system=system,
|
| 46 |
+
)
|
| 47 |
+
return msg.content[0].text.strip()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _extract_claims(client: anthropic.Anthropic, doc: Document, n: int = 12) -> list[dict]:
|
| 51 |
+
# Use the full document text but cap at ~6000 words to stay within limits
|
| 52 |
+
text = " ".join(doc.raw.split()[:6000])
|
| 53 |
+
prompt = _CLAIM_PROMPT.format(n=n, document=text)
|
| 54 |
+
raw = _call_claude(client, "You extract factual claims from documents.", prompt)
|
| 55 |
+
# Strip markdown code fences if present
|
| 56 |
+
raw = raw.strip()
|
| 57 |
+
if raw.startswith("```"):
|
| 58 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 59 |
+
raw = raw.rsplit("```", 1)[0]
|
| 60 |
+
try:
|
| 61 |
+
claims = json.loads(raw)
|
| 62 |
+
return claims[:n]
|
| 63 |
+
except json.JSONDecodeError:
|
| 64 |
+
return []
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _closed_book_answer(client: anthropic.Anthropic, question: str) -> str:
|
| 68 |
+
return _call_claude(
|
| 69 |
+
client,
|
| 70 |
+
"Answer the question using only your pre-trained knowledge. Do not make up information. If unsure, say so.",
|
| 71 |
+
question,
|
| 72 |
+
model="claude-haiku-4-5-20251001", # cheaper for bulk closed-book queries
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _judge_novelty(client: anthropic.Anthropic, claim: str, answer: str) -> dict:
|
| 77 |
+
raw = _call_claude(
|
| 78 |
+
client,
|
| 79 |
+
"You are an expert evaluator assessing AI knowledge coverage.",
|
| 80 |
+
_JUDGE_PROMPT.format(claim=claim, answer=answer),
|
| 81 |
+
)
|
| 82 |
+
raw = raw.strip()
|
| 83 |
+
if raw.startswith("```"):
|
| 84 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 85 |
+
raw = raw.rsplit("```", 1)[0]
|
| 86 |
+
try:
|
| 87 |
+
return json.loads(raw)
|
| 88 |
+
except json.JSONDecodeError:
|
| 89 |
+
return {"score": 0.5, "reason": "Could not parse judge response."}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def evaluate(client: anthropic.Anthropic, doc: Document, progress_cb=None) -> dict:
|
| 93 |
+
"""Return novelty score (0-100) and detailed results."""
|
| 94 |
+
if progress_cb:
|
| 95 |
+
progress_cb("Extracting factual claims from document...")
|
| 96 |
+
|
| 97 |
+
claims = _extract_claims(client, doc)
|
| 98 |
+
if not claims:
|
| 99 |
+
return {"score": 50, "details": [], "summary": "Could not extract claims from document."}
|
| 100 |
+
|
| 101 |
+
results = []
|
| 102 |
+
for i, item in enumerate(claims):
|
| 103 |
+
if progress_cb:
|
| 104 |
+
progress_cb(f"Testing claim {i+1}/{len(claims)} against model knowledge...")
|
| 105 |
+
answer = _closed_book_answer(client, item["question"])
|
| 106 |
+
judgment = _judge_novelty(client, item["claim"], answer)
|
| 107 |
+
results.append({
|
| 108 |
+
"claim": item["claim"],
|
| 109 |
+
"question": item["question"],
|
| 110 |
+
"model_answer": answer,
|
| 111 |
+
"known_score": judgment["score"],
|
| 112 |
+
"reason": judgment.get("reason", ""),
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
avg_known = sum(r["known_score"] for r in results) / len(results)
|
| 116 |
+
novelty_score = round((1 - avg_known) * 100)
|
| 117 |
+
|
| 118 |
+
return {
|
| 119 |
+
"score": novelty_score,
|
| 120 |
+
"details": results,
|
| 121 |
+
"summary": f"Tested {len(results)} claims. Model already knows ~{round(avg_known*100)}% of this knowledge.",
|
| 122 |
+
}
|
kvl/modules/retrieval.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module B: Retrieval Utility β measures how well the document supports information retrieval."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import json
|
| 5 |
+
import numpy as np
|
| 6 |
+
import anthropic
|
| 7 |
+
from kvl.ingestor import Document
|
| 8 |
+
|
| 9 |
+
_QUERY_PROMPT = """Generate {n} realistic search queries that a user would type to find information in this document.
|
| 10 |
+
Queries should be specific, not generic. Mix question and keyword styles.
|
| 11 |
+
|
| 12 |
+
Return ONLY a JSON array of strings.
|
| 13 |
+
|
| 14 |
+
Example: ["What is the impact of X on Y?", "methodology for measuring Z", ...]
|
| 15 |
+
|
| 16 |
+
Document excerpt:
|
| 17 |
+
{excerpt}"""
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _call_claude(client: anthropic.Anthropic, prompt: str) -> str:
|
| 21 |
+
msg = client.messages.create(
|
| 22 |
+
model="claude-haiku-4-5-20251001",
|
| 23 |
+
max_tokens=1024,
|
| 24 |
+
messages=[{"role": "user", "content": prompt}],
|
| 25 |
+
)
|
| 26 |
+
return msg.content[0].text.strip()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _generate_queries(client: anthropic.Anthropic, doc: Document, n: int = 8) -> list[str]:
|
| 30 |
+
excerpt = " ".join(doc.raw.split()[:3000])
|
| 31 |
+
raw = _call_claude(client, _QUERY_PROMPT.format(n=n, excerpt=excerpt))
|
| 32 |
+
raw = raw.strip()
|
| 33 |
+
if raw.startswith("```"):
|
| 34 |
+
raw = "\n".join(raw.split("\n")[1:])
|
| 35 |
+
raw = raw.rsplit("```", 1)[0]
|
| 36 |
+
try:
|
| 37 |
+
queries = json.loads(raw)
|
| 38 |
+
return [q for q in queries if isinstance(q, str)][:n]
|
| 39 |
+
except json.JSONDecodeError:
|
| 40 |
+
return []
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _build_index(chunks: list, embedder) -> tuple:
|
| 44 |
+
"""Embed all chunks and build a FAISS flat index."""
|
| 45 |
+
import faiss
|
| 46 |
+
texts = [c.text for c in chunks]
|
| 47 |
+
embeddings = embedder.encode(texts, normalize_embeddings=True, show_progress_bar=False)
|
| 48 |
+
embeddings = np.array(embeddings, dtype="float32")
|
| 49 |
+
dim = embeddings.shape[1]
|
| 50 |
+
index = faiss.IndexFlatIP(dim) # inner product = cosine when normalized
|
| 51 |
+
index.add(embeddings)
|
| 52 |
+
return index, embeddings
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _retrieve(query: str, index, embedder, k: int = 3) -> list[int]:
|
| 56 |
+
"""Return top-k chunk indices for a query."""
|
| 57 |
+
q_emb = embedder.encode([query], normalize_embeddings=True, show_progress_bar=False)
|
| 58 |
+
q_emb = np.array(q_emb, dtype="float32")
|
| 59 |
+
_, indices = index.search(q_emb, k)
|
| 60 |
+
return indices[0].tolist()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _recall_at_k(retrieved: list[int], relevant: int) -> float:
|
| 64 |
+
return 1.0 if relevant in retrieved else 0.0
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _reciprocal_rank(retrieved: list[int], relevant: int) -> float:
|
| 68 |
+
for rank, idx in enumerate(retrieved, start=1):
|
| 69 |
+
if idx == relevant:
|
| 70 |
+
return 1.0 / rank
|
| 71 |
+
return 0.0
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def evaluate(client: anthropic.Anthropic, doc: Document, embedder, progress_cb=None) -> dict:
|
| 75 |
+
"""Return retrieval utility score (0-100) and detailed results."""
|
| 76 |
+
if not doc.chunks:
|
| 77 |
+
return {"score": 0, "details": [], "summary": "No chunks to index."}
|
| 78 |
+
|
| 79 |
+
if progress_cb:
|
| 80 |
+
progress_cb("Building retrieval index...")
|
| 81 |
+
|
| 82 |
+
index, _ = _build_index(doc.chunks, embedder)
|
| 83 |
+
|
| 84 |
+
if progress_cb:
|
| 85 |
+
progress_cb("Generating retrieval test queries...")
|
| 86 |
+
|
| 87 |
+
queries = _generate_queries(client, doc)
|
| 88 |
+
if not queries:
|
| 89 |
+
return {"score": 50, "details": [], "summary": "Could not generate test queries."}
|
| 90 |
+
|
| 91 |
+
results = []
|
| 92 |
+
recall_scores = []
|
| 93 |
+
mrr_scores = []
|
| 94 |
+
|
| 95 |
+
for i, query in enumerate(queries):
|
| 96 |
+
if progress_cb:
|
| 97 |
+
progress_cb(f"Evaluating retrieval query {i+1}/{len(queries)}...")
|
| 98 |
+
|
| 99 |
+
retrieved_indices = _retrieve(query, index, embedder, k=3)
|
| 100 |
+
|
| 101 |
+
# Find the most semantically relevant chunk by re-ranking with cosine sim
|
| 102 |
+
q_emb = embedder.encode([query], normalize_embeddings=True, show_progress_bar=False)
|
| 103 |
+
q_emb = np.array(q_emb, dtype="float32")
|
| 104 |
+
chunk_texts = [c.text for c in doc.chunks]
|
| 105 |
+
chunk_embs = embedder.encode(chunk_texts, normalize_embeddings=True, show_progress_bar=False)
|
| 106 |
+
sims = np.dot(chunk_embs, q_emb.T).flatten()
|
| 107 |
+
best_chunk = int(np.argmax(sims))
|
| 108 |
+
|
| 109 |
+
r_at_3 = _recall_at_k(retrieved_indices, best_chunk)
|
| 110 |
+
rr = _reciprocal_rank(retrieved_indices, best_chunk)
|
| 111 |
+
recall_scores.append(r_at_3)
|
| 112 |
+
mrr_scores.append(rr)
|
| 113 |
+
|
| 114 |
+
results.append({
|
| 115 |
+
"query": query,
|
| 116 |
+
"retrieved_chunks": retrieved_indices,
|
| 117 |
+
"best_chunk": best_chunk,
|
| 118 |
+
"recall_at_3": r_at_3,
|
| 119 |
+
"reciprocal_rank": rr,
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
avg_recall = sum(recall_scores) / len(recall_scores)
|
| 123 |
+
avg_mrr = sum(mrr_scores) / len(mrr_scores)
|
| 124 |
+
# Weighted combination: recall@3 (60%) + MRR (40%), mapped to 0-100
|
| 125 |
+
raw_score = 0.6 * avg_recall + 0.4 * avg_mrr
|
| 126 |
+
score = round(raw_score * 100)
|
| 127 |
+
|
| 128 |
+
return {
|
| 129 |
+
"score": score,
|
| 130 |
+
"details": results,
|
| 131 |
+
"summary": f"Recall@3: {avg_recall:.2f} | MRR: {avg_mrr:.2f} across {len(queries)} queries.",
|
| 132 |
+
}
|
kvl/report.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate a Markdown Knowledge Value Report from evaluation results."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from kvl.config import DIMENSION_META, KVS_CLASSIFICATION, MODELS
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _bar(score: int, width: int = 20) -> str:
|
| 9 |
+
filled = round(score / 100 * width)
|
| 10 |
+
return "β" * filled + "β" * (width - filled)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _sensitivity_badge(level: str) -> str:
|
| 14 |
+
icons = {"High": "π΄", "Moderate": "π‘", "Low": "π’"}
|
| 15 |
+
return f"{icons.get(level, '')} {level}"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def generate(doc_title: str, kvs_result: dict, module_results: dict, meta: dict | None = None) -> str:
|
| 19 |
+
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 20 |
+
kvs = kvs_result["kvs"]
|
| 21 |
+
classification = kvs_result["classification"]
|
| 22 |
+
dim_scores = kvs_result["dimension_scores"]
|
| 23 |
+
contributions = kvs_result["weighted_contributions"]
|
| 24 |
+
recommendations = kvs_result["recommendations"]
|
| 25 |
+
|
| 26 |
+
eval_date = (meta or {}).get("eval_date", now)
|
| 27 |
+
framework_version = (meta or {}).get("framework_version", "KVL v0.1")
|
| 28 |
+
|
| 29 |
+
lines = [
|
| 30 |
+
"# Knowledge Value Report",
|
| 31 |
+
"",
|
| 32 |
+
f"**Document:** {doc_title} ",
|
| 33 |
+
f"**Evaluated:** {eval_date} ",
|
| 34 |
+
f"**Framework:** {framework_version}",
|
| 35 |
+
"",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
# ββ Model Metadata block ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
lines += [
|
| 40 |
+
"## Evaluation Models",
|
| 41 |
+
"",
|
| 42 |
+
"> β οΈ **Score validity notice:** Knowledge Novelty and Generation Utility scores are",
|
| 43 |
+
"> **model-relative** β they measure the marginal value of this document to the specific",
|
| 44 |
+
"> AI models listed below. Scores will change if these models are updated or replaced.",
|
| 45 |
+
"> Retrieval Utility and Demand Utility are largely model-independent.",
|
| 46 |
+
"> Re-evaluate after any major model version change.",
|
| 47 |
+
"",
|
| 48 |
+
"| Role | Model | Dimensions |",
|
| 49 |
+
"|---|---|---|",
|
| 50 |
+
f"| Evaluation judge | {MODELS['judge']['display']} (`{MODELS['judge']['id']}`) | Novelty scoring, Grounding, Demand |",
|
| 51 |
+
f"| Answer generation | {MODELS['worker']['display']} (`{MODELS['worker']['id']}`) | Closed-book QA, Baseline & RAG answers, Queries |",
|
| 52 |
+
f"| Text embeddings | {MODELS['embedder']['display']} | Retrieval index, Semantic similarity |",
|
| 53 |
+
"",
|
| 54 |
+
"---",
|
| 55 |
+
"",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
# ββ KVS Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
lines += [
|
| 60 |
+
"## Overall Knowledge Value Score",
|
| 61 |
+
"",
|
| 62 |
+
"```",
|
| 63 |
+
f" {kvs} / 100 β {classification}",
|
| 64 |
+
f" {_bar(kvs)}",
|
| 65 |
+
"```",
|
| 66 |
+
"",
|
| 67 |
+
"| Band | Range | Description |",
|
| 68 |
+
"|---|---|---|",
|
| 69 |
+
]
|
| 70 |
+
for threshold, label, desc in KVS_CLASSIFICATION:
|
| 71 |
+
marker = " β this document" if label == classification else ""
|
| 72 |
+
lines.append(f"| **{label}** | {threshold}β{threshold+19 if threshold < 81 else 100} | {desc}{marker} |")
|
| 73 |
+
lines += ["", "---", ""]
|
| 74 |
+
|
| 75 |
+
# ββ Dimension table βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
+
lines += [
|
| 77 |
+
"## Dimension Scores",
|
| 78 |
+
"",
|
| 79 |
+
"| Dimension | Score | Bar | Weight | Contribution | Model Sensitivity |",
|
| 80 |
+
"|---|---|---|---|---|---|",
|
| 81 |
+
]
|
| 82 |
+
for key, dmeta in DIMENSION_META.items():
|
| 83 |
+
sc = dim_scores[key]
|
| 84 |
+
contrib = contributions[key]
|
| 85 |
+
sens = _sensitivity_badge(dmeta["model_sensitivity"])
|
| 86 |
+
lines.append(
|
| 87 |
+
f"| {dmeta['name']} | {sc}/100 | `{_bar(sc, 10)}` "
|
| 88 |
+
f"| {int(dmeta['weight']*100)}% | {contrib} | {sens} |"
|
| 89 |
+
)
|
| 90 |
+
lines += ["", "---", ""]
|
| 91 |
+
|
| 92 |
+
# ββ Per-dimension detail ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 93 |
+
section_order = ["novelty", "retrieval", "generation", "attribution", "demand"]
|
| 94 |
+
section_titles = {
|
| 95 |
+
"novelty": "Knowledge Novelty",
|
| 96 |
+
"retrieval": "Retrieval Utility",
|
| 97 |
+
"generation": "Generation Utility",
|
| 98 |
+
"attribution": "Attribution & Grounding",
|
| 99 |
+
"demand": "Demand Utility",
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
for key in section_order:
|
| 103 |
+
dmeta = DIMENSION_META[key]
|
| 104 |
+
sc = dim_scores[key]
|
| 105 |
+
sens = _sensitivity_badge(dmeta["model_sensitivity"])
|
| 106 |
+
lines += [
|
| 107 |
+
f"## {section_titles[key]}",
|
| 108 |
+
"",
|
| 109 |
+
f"**Score: {sc}/100** | Model sensitivity: {sens}",
|
| 110 |
+
"",
|
| 111 |
+
f"_{dmeta['description']}_",
|
| 112 |
+
"",
|
| 113 |
+
f"**How measured:** {dmeta['how_measured']}",
|
| 114 |
+
"",
|
| 115 |
+
f"**Models used:** {', '.join(dmeta['models_used'])}",
|
| 116 |
+
"",
|
| 117 |
+
f"**Model sensitivity note:** {dmeta['sensitivity_note']}",
|
| 118 |
+
"",
|
| 119 |
+
f"{module_results.get(key, {}).get('summary', '')}",
|
| 120 |
+
"",
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
if key == "novelty":
|
| 124 |
+
details = module_results.get("novelty", {}).get("details", [])
|
| 125 |
+
if details:
|
| 126 |
+
lines += [
|
| 127 |
+
f"<details><summary>Claim-by-claim breakdown ({len(details)} claims)</summary>",
|
| 128 |
+
"",
|
| 129 |
+
"| Claim | Known Score | Notes |",
|
| 130 |
+
"|---|---|---|",
|
| 131 |
+
]
|
| 132 |
+
for d in details:
|
| 133 |
+
lines.append(f"| {d['claim'][:80]} | {d['known_score']:.2f} | {d['reason'][:60]} |")
|
| 134 |
+
lines += ["", "</details>", ""]
|
| 135 |
+
|
| 136 |
+
elif key == "retrieval":
|
| 137 |
+
details = module_results.get("retrieval", {}).get("details", [])
|
| 138 |
+
if details:
|
| 139 |
+
lines += [
|
| 140 |
+
f"<details><summary>Query-by-query results ({len(details)} queries)</summary>",
|
| 141 |
+
"",
|
| 142 |
+
"| Query | Recall@3 | MRR |",
|
| 143 |
+
"|---|---|---|",
|
| 144 |
+
]
|
| 145 |
+
for d in details:
|
| 146 |
+
lines.append(f"| {d['query'][:80]} | {d['recall_at_3']:.2f} | {d['reciprocal_rank']:.2f} |")
|
| 147 |
+
lines += ["", "</details>", ""]
|
| 148 |
+
|
| 149 |
+
elif key == "generation":
|
| 150 |
+
details = module_results.get("generation", {}).get("details", [])
|
| 151 |
+
if details:
|
| 152 |
+
lines += [
|
| 153 |
+
f"<details><summary>Question-by-question results ({len(details)} questions)</summary>",
|
| 154 |
+
"",
|
| 155 |
+
"| Question | Improvement | Reason |",
|
| 156 |
+
"|---|---|---|",
|
| 157 |
+
]
|
| 158 |
+
for d in details:
|
| 159 |
+
lines.append(f"| {d['question'][:80]} | {d['improvement']}/100 | {d['reason'][:60]} |")
|
| 160 |
+
lines += ["", "</details>", ""]
|
| 161 |
+
|
| 162 |
+
elif key == "attribution":
|
| 163 |
+
details = module_results.get("attribution", {}).get("details", [])
|
| 164 |
+
if details:
|
| 165 |
+
lines += [
|
| 166 |
+
f"<details><summary>Per-answer grounding ({len(details)} answers)</summary>",
|
| 167 |
+
"",
|
| 168 |
+
"| Question | Grounding % | Hallucination | Semantic Sim |",
|
| 169 |
+
"|---|---|---|---|",
|
| 170 |
+
]
|
| 171 |
+
for d in details:
|
| 172 |
+
halluc = "Yes β οΈ" if d.get("hallucination_detected") else "No"
|
| 173 |
+
lines.append(
|
| 174 |
+
f"| {d.get('question','')[:70]} "
|
| 175 |
+
f"| {round(d['grounding_fraction']*100)}% "
|
| 176 |
+
f"| {halluc} "
|
| 177 |
+
f"| {d['semantic_similarity']} |"
|
| 178 |
+
)
|
| 179 |
+
lines += ["", "</details>", ""]
|
| 180 |
+
|
| 181 |
+
elif key == "demand":
|
| 182 |
+
topics = module_results.get("demand", {}).get("topics", [])
|
| 183 |
+
if topics:
|
| 184 |
+
lines += [
|
| 185 |
+
"| Topic | Query Freq (1-10) | Priority Domain | Unmet Need |",
|
| 186 |
+
"|---|---|---|---|",
|
| 187 |
+
]
|
| 188 |
+
for t in topics:
|
| 189 |
+
lines.append(
|
| 190 |
+
f"| {t.get('topic','')[:50]} "
|
| 191 |
+
f"| {t.get('query_frequency','-')}/10 "
|
| 192 |
+
f"| {'Yes' if t.get('priority_domain') else 'No'} "
|
| 193 |
+
f"| {'Yes' if t.get('unmet_need') else 'No'} |"
|
| 194 |
+
)
|
| 195 |
+
lines.append("")
|
| 196 |
+
|
| 197 |
+
lines += ["---", ""]
|
| 198 |
+
|
| 199 |
+
# ββ Recommendations βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 200 |
+
lines += ["## Recommended Actions", ""]
|
| 201 |
+
for rec in recommendations:
|
| 202 |
+
lines.append(f"- {rec}")
|
| 203 |
+
|
| 204 |
+
lines += [
|
| 205 |
+
"",
|
| 206 |
+
"---",
|
| 207 |
+
"",
|
| 208 |
+
f"*Generated by {framework_version} Β· Evaluated {eval_date}* ",
|
| 209 |
+
f"*Judge: {MODELS['judge']['display']} Β· Worker: {MODELS['worker']['display']} Β· Embeddings: {MODELS['embedder']['display']}* ",
|
| 210 |
+
"*Scores are model-relative. Re-evaluate after major model updates.*",
|
| 211 |
+
]
|
| 212 |
+
|
| 213 |
+
return "\n".join(lines)
|
kvl/scorer.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KVS aggregation β combines five dimension scores into the Knowledge Value Score."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
WEIGHTS = {
|
| 6 |
+
"novelty": 0.30,
|
| 7 |
+
"retrieval": 0.20,
|
| 8 |
+
"generation": 0.25,
|
| 9 |
+
"attribution": 0.15,
|
| 10 |
+
"demand": 0.10,
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
CLASSIFICATION = [
|
| 14 |
+
(81, "Transformational Value"),
|
| 15 |
+
(61, "High Value"),
|
| 16 |
+
(41, "Moderate Value"),
|
| 17 |
+
(21, "Incremental Value"),
|
| 18 |
+
(0, "Minimal Value"),
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
RECOMMENDATIONS = {
|
| 22 |
+
"high_novelty": "Prioritize indexing in AI retrieval systems β contains knowledge not in foundation models.",
|
| 23 |
+
"low_novelty": "Widely known content; consider whether curation effort is justified.",
|
| 24 |
+
"high_retrieval": "Well-structured for retrieval β integrate directly into RAG pipelines.",
|
| 25 |
+
"low_retrieval": "Improve chunking, structure, or metadata to boost retrievability.",
|
| 26 |
+
"high_generation": "Strongly improves AI-generated answers β valuable for advisory and QA systems.",
|
| 27 |
+
"low_generation": "Limited generation uplift β content may be too abstract or redundant.",
|
| 28 |
+
"high_attribution": "Answers are well-grounded in document β high trustworthiness for deployment.",
|
| 29 |
+
"low_attribution": "Grounding is weak β review document structure and specificity.",
|
| 30 |
+
"high_demand": "High user demand β wide deployment and open access recommended.",
|
| 31 |
+
"low_demand": "Narrow or specialized demand β consider targeted distribution.",
|
| 32 |
+
"translate": "Consider translation into additional languages to broaden impact.",
|
| 33 |
+
"open_access": "Recommend open access publication to maximize societal return.",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def classify(score: int) -> str:
|
| 38 |
+
for threshold, label in CLASSIFICATION:
|
| 39 |
+
if score >= threshold:
|
| 40 |
+
return label
|
| 41 |
+
return "Minimal Value"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def compute(scores: dict[str, int]) -> dict:
|
| 45 |
+
"""Compute weighted KVS from dimension scores."""
|
| 46 |
+
kvs = sum(scores[dim] * weight for dim, weight in WEIGHTS.items())
|
| 47 |
+
kvs = round(kvs)
|
| 48 |
+
|
| 49 |
+
weighted_contributions = {
|
| 50 |
+
dim: round(scores[dim] * weight, 1)
|
| 51 |
+
for dim, weight in WEIGHTS.items()
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
recommendations = _recommend(scores)
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
"kvs": kvs,
|
| 58 |
+
"classification": classify(kvs),
|
| 59 |
+
"dimension_scores": scores,
|
| 60 |
+
"weighted_contributions": weighted_contributions,
|
| 61 |
+
"recommendations": recommendations,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _recommend(scores: dict[str, int]) -> list[str]:
|
| 66 |
+
recs = []
|
| 67 |
+
if scores.get("novelty", 50) >= 65:
|
| 68 |
+
recs.append(RECOMMENDATIONS["high_novelty"])
|
| 69 |
+
else:
|
| 70 |
+
recs.append(RECOMMENDATIONS["low_novelty"])
|
| 71 |
+
|
| 72 |
+
if scores.get("retrieval", 50) < 50:
|
| 73 |
+
recs.append(RECOMMENDATIONS["low_retrieval"])
|
| 74 |
+
else:
|
| 75 |
+
recs.append(RECOMMENDATIONS["high_retrieval"])
|
| 76 |
+
|
| 77 |
+
if scores.get("generation", 50) >= 60:
|
| 78 |
+
recs.append(RECOMMENDATIONS["high_generation"])
|
| 79 |
+
else:
|
| 80 |
+
recs.append(RECOMMENDATIONS["low_generation"])
|
| 81 |
+
|
| 82 |
+
if scores.get("attribution", 50) < 55:
|
| 83 |
+
recs.append(RECOMMENDATIONS["low_attribution"])
|
| 84 |
+
else:
|
| 85 |
+
recs.append(RECOMMENDATIONS["high_attribution"])
|
| 86 |
+
|
| 87 |
+
if scores.get("demand", 50) >= 60:
|
| 88 |
+
recs.append(RECOMMENDATIONS["high_demand"])
|
| 89 |
+
recs.append(RECOMMENDATIONS["open_access"])
|
| 90 |
+
else:
|
| 91 |
+
recs.append(RECOMMENDATIONS["low_demand"])
|
| 92 |
+
|
| 93 |
+
if scores.get("novelty", 50) >= 70:
|
| 94 |
+
recs.append(RECOMMENDATIONS["translate"])
|
| 95 |
+
|
| 96 |
+
return recs
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
anthropic>=0.40.0
|
| 2 |
+
streamlit>=1.40.0
|
| 3 |
+
sentence-transformers>=3.0.0
|
| 4 |
+
faiss-cpu>=1.8.0
|
| 5 |
+
python-dotenv>=1.0.0
|
| 6 |
+
markdown-it-py>=3.0.0
|
| 7 |
+
numpy>=1.26.0
|