Sujatha commited on
Commit
a45076c
·
verified ·
1 Parent(s): b1ad332

Create knowledge_engine.py

Browse files
Files changed (1) hide show
  1. knowledge_engine.py +565 -0
knowledge_engine.py ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Knowledge Engine - Layer 2: Knowledge management and retrieval
3
+ """
4
+
5
+ import json
6
+ import numpy as np
7
+ from typing import Dict, List, Any, Optional, Tuple
8
+ from datetime import datetime
9
+ import hashlib
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+
13
+
14
+ class KnowledgeType(Enum):
15
+ """Types of knowledge in the system"""
16
+ CONCEPT = "concept"
17
+ FORMULA = "formula"
18
+ EXAMPLE = "example"
19
+ PRACTICE_PROBLEM = "practice_problem"
20
+ STUDY_GUIDE = "study_guide"
21
+ EXAM_TIP = "exam_tip"
22
+
23
+
24
+ @dataclass
25
+ class KnowledgeNode:
26
+ """Knowledge node in the knowledge graph"""
27
+ id: str
28
+ type: KnowledgeType
29
+ content: Dict[str, Any]
30
+ topics: List[str]
31
+ difficulty: str
32
+ prerequisites: List[str]
33
+ related_nodes: List[str]
34
+ metadata: Dict[str, Any]
35
+ created_at: datetime
36
+ last_accessed: datetime
37
+ access_count: int
38
+
39
+
40
+ class KnowledgeEngine:
41
+ """Knowledge management and retrieval engine"""
42
+
43
+ def __init__(self, knowledge_base_path: str = "data/cfa_knowledge_base.json"):
44
+ self.knowledge_graph: Dict[str, KnowledgeNode] = {}
45
+ self.topic_index: Dict[str, List[str]] = {}
46
+ self.concept_index: Dict[str, List[str]] = {}
47
+ self.load_knowledge_base(knowledge_base_path)
48
+ self.access_log = []
49
+
50
+ def load_knowledge_base(self, path: str):
51
+ """Load and index knowledge base"""
52
+ try:
53
+ with open(path, 'r', encoding='utf-8') as f:
54
+ knowledge_data = json.load(f)
55
+ self._build_knowledge_graph(knowledge_data)
56
+ print(f"✅ Knowledge base loaded: {len(self.knowledge_graph)} nodes")
57
+ except FileNotFoundError:
58
+ print(f"⚠️ Knowledge base not found at {path}, creating minimal graph")
59
+ self._create_minimal_graph()
60
+
61
+ def _build_knowledge_graph(self, knowledge_data: Dict[str, Any]):
62
+ """Build knowledge graph from loaded data"""
63
+ # Process topics
64
+ for topic_name, topic_data in knowledge_data.get('topics', {}).items():
65
+ # Create concept nodes
66
+ concepts = topic_data.get('key_concepts', [])
67
+ for concept in concepts:
68
+ node_id = self._generate_node_id(concept['name'], KnowledgeType.CONCEPT)
69
+ node = KnowledgeNode(
70
+ id=node_id,
71
+ type=KnowledgeType.CONCEPT,
72
+ content={
73
+ 'name': concept['name'],
74
+ 'definition': concept.get('definition', ''),
75
+ 'formula': concept.get('formula', ''),
76
+ 'example': concept.get('example', '')
77
+ },
78
+ topics=[topic_name],
79
+ difficulty=topic_data.get('difficulty', 'medium'),
80
+ prerequisites=[],
81
+ related_nodes=[],
82
+ metadata={
83
+ 'cfa_level': topic_data.get('cfa_level', 'I'),
84
+ 'weight': topic_data.get('weight', ''),
85
+ 'applications': topic_data.get('common_applications', [])
86
+ },
87
+ created_at=datetime.now(),
88
+ last_accessed=datetime.now(),
89
+ access_count=0
90
+ )
91
+ self.knowledge_graph[node_id] = node
92
+
93
+ # Update indices
94
+ self._add_to_index(node_id, topic_name, concept['name'])
95
+
96
+ # Process formulas
97
+ for category, formulas in knowledge_data.get('formulas', {}).items():
98
+ for formula in formulas:
99
+ node_id = self._generate_node_id(formula['name'], KnowledgeType.FORMULA)
100
+ node = KnowledgeNode(
101
+ id=node_id,
102
+ type=KnowledgeType.FORMULA,
103
+ content={
104
+ 'name': formula['name'],
105
+ 'formula': formula.get('formula', ''),
106
+ 'description': formula.get('description', ''),
107
+ 'variables': formula.get('variables', {})
108
+ },
109
+ topics=[category],
110
+ difficulty='medium',
111
+ prerequisites=[],
112
+ related_nodes=[],
113
+ metadata={'category': category},
114
+ created_at=datetime.now(),
115
+ last_accessed=datetime.now(),
116
+ access_count=0
117
+ )
118
+ self.knowledge_graph[node_id] = node
119
+ self._add_to_index(node_id, category, formula['name'])
120
+
121
+ # Build relationships
122
+ self._build_relationships()
123
+
124
+ def _create_minimal_graph(self):
125
+ """Create minimal knowledge graph if no data file"""
126
+ # Core TVM concepts
127
+ tvm_concepts = [
128
+ {
129
+ 'name': 'Present Value',
130
+ 'definition': 'Current worth of future cash flows',
131
+ 'formula': 'PV = FV / (1 + r)^n'
132
+ },
133
+ {
134
+ 'name': 'Future Value',
135
+ 'definition': 'Future worth of current investment',
136
+ 'formula': 'FV = PV × (1 + r)^n'
137
+ }
138
+ ]
139
+
140
+ for concept in tvm_concepts:
141
+ node_id = self._generate_node_id(concept['name'], KnowledgeType.CONCEPT)
142
+ node = KnowledgeNode(
143
+ id=node_id,
144
+ type=KnowledgeType.CONCEPT,
145
+ content=concept,
146
+ topics=['Time Value of Money'],
147
+ difficulty='medium',
148
+ prerequisites=[],
149
+ related_nodes=[],
150
+ metadata={'cfa_level': 'I', 'weight': '8-12%'},
151
+ created_at=datetime.now(),
152
+ last_accessed=datetime.now(),
153
+ access_count=0
154
+ )
155
+ self.knowledge_graph[node_id] = node
156
+ self._add_to_index(node_id, 'Time Value of Money', concept['name'])
157
+
158
+ def _generate_node_id(self, name: str, ktype: KnowledgeType) -> str:
159
+ """Generate unique node ID"""
160
+ hash_input = f"{name}_{ktype.value}_{datetime.now().timestamp()}"
161
+ return hashlib.md5(hash_input.encode()).hexdigest()[:12]
162
+
163
+ def _add_to_index(self, node_id: str, topic: str, concept: str):
164
+ """Add node to search indices"""
165
+ # Topic index
166
+ if topic not in self.topic_index:
167
+ self.topic_index[topic] = []
168
+ self.topic_index[topic].append(node_id)
169
+
170
+ # Concept index (by keywords)
171
+ keywords = concept.lower().split()
172
+ for keyword in keywords:
173
+ if len(keyword) > 3: # Ignore short words
174
+ if keyword not in self.concept_index:
175
+ self.concept_index[keyword] = []
176
+ self.concept_index[keyword].append(node_id)
177
+
178
+ def _build_relationships(self):
179
+ """Build relationships between knowledge nodes"""
180
+ # Simple relationship building - in production, use NLP
181
+ node_ids = list(self.knowledge_graph.keys())
182
+
183
+ for i, node_id in enumerate(node_ids):
184
+ node = self.knowledge_graph[node_id]
185
+ related = []
186
+
187
+ # Find related nodes by topic
188
+ for other_id in node_ids:
189
+ if other_id != node_id:
190
+ other_node = self.knowledge_graph[other_id]
191
+ # Same topic
192
+ if any(topic in other_node.topics for topic in node.topics):
193
+ related.append(other_id)
194
+ # Related concepts (simple keyword matching)
195
+ node_content = str(node.content).lower()
196
+ other_content = str(other_node.content).lower()
197
+ common_words = set(node_content.split()) & set(other_content.split())
198
+ if len(common_words) >= 2:
199
+ related.append(other_id)
200
+
201
+ # Limit relationships
202
+ node.related_nodes = list(set(related))[:10]
203
+ self.knowledge_graph[node_id] = node
204
+
205
+ def retrieve_knowledge(self,
206
+ query: str,
207
+ user_profile: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
208
+ """
209
+ Retrieve knowledge based on query
210
+
211
+ Args:
212
+ query: Knowledge query
213
+ user_profile: User's learning profile
214
+
215
+ Returns:
216
+ Retrieved knowledge
217
+ """
218
+ # Log access
219
+ self.access_log.append({
220
+ 'query': query,
221
+ 'timestamp': datetime.now().isoformat(),
222
+ 'user_profile': user_profile
223
+ })
224
+
225
+ # Search for relevant nodes
226
+ search_results = self._search_knowledge(query)
227
+
228
+ # Rank results
229
+ ranked_results = self._rank_results(search_results, query, user_profile)
230
+
231
+ # Prepare response
232
+ response = self._prepare_knowledge_response(ranked_results, query, user_profile)
233
+
234
+ # Update access counts
235
+ for node_id, _ in ranked_results[:5]:
236
+ if node_id in self.knowledge_graph:
237
+ node = self.knowledge_graph[node_id]
238
+ node.last_accessed = datetime.now()
239
+ node.access_count += 1
240
+ self.knowledge_graph[node_id] = node
241
+
242
+ return response
243
+
244
+ def _search_knowledge(self, query: str) -> List[str]:
245
+ """Search knowledge graph for relevant nodes"""
246
+ query_lower = query.lower()
247
+ found_nodes = set()
248
+
249
+ # Search by topic
250
+ for topic in self.topic_index.keys():
251
+ if topic.lower() in query_lower:
252
+ found_nodes.update(self.topic_index[topic])
253
+
254
+ # Search by keywords
255
+ query_words = query_lower.split()
256
+ for word in query_words:
257
+ if len(word) > 3 and word in self.concept_index:
258
+ found_nodes.update(self.concept_index[word])
259
+
260
+ # If no results, try broader search
261
+ if not found_nodes:
262
+ for node_id, node in self.knowledge_graph.items():
263
+ node_text = str(node.content).lower()
264
+ if any(word in node_text for word in query_words[:3]):
265
+ found_nodes.add(node_id)
266
+
267
+ return list(found_nodes)
268
+
269
+ def _rank_results(self,
270
+ node_ids: List[str],
271
+ query: str,
272
+ user_profile: Optional[Dict[str, Any]]) -> List[Tuple[str, float]]:
273
+ """Rank search results by relevance"""
274
+ ranked = []
275
+
276
+ for node_id in node_ids:
277
+ node = self.knowledge_graph.get(node_id)
278
+ if not node:
279
+ continue
280
+
281
+ relevance_score = self._calculate_relevance(node, query, user_profile)
282
+ ranked.append((node_id, relevance_score))
283
+
284
+ # Sort by relevance
285
+ ranked.sort(key=lambda x: x[1], reverse=True)
286
+ return ranked
287
+
288
+ def _calculate_relevance(self,
289
+ node: KnowledgeNode,
290
+ query: str,
291
+ user_profile: Optional[Dict[str, Any]]) -> float:
292
+ """Calculate relevance score for a node"""
293
+ score = 0.0
294
+
295
+ # Text similarity (simplified)
296
+ query_lower = query.lower()
297
+ node_text = str(node.content).lower()
298
+
299
+ # Keyword matches
300
+ query_words = set(query_lower.split())
301
+ node_words = set(node_text.split())
302
+ keyword_overlap = len(query_words & node_words) / max(len(query_words), 1)
303
+ score += keyword_overlap * 0.4
304
+
305
+ # Topic relevance
306
+ if user_profile:
307
+ study_focus = user_profile.get('study_focus', [])
308
+ topic_overlap = len(set(node.topics) & set(study_focus))
309
+ score += topic_overlap * 0.2
310
+
311
+ # Popularity/recency
312
+ recency_factor = 1.0 - ((datetime.now() - node.last_accessed).days / 365)
313
+ popularity_factor = min(node.access_count / 100, 1.0)
314
+ score += (recency_factor * 0.2 + popularity_factor * 0.2)
315
+
316
+ # Difficulty match
317
+ if user_profile:
318
+ experience = user_profile.get('experience_level', 'beginner')
319
+ difficulty_score = self._match_difficulty(node.difficulty, experience)
320
+ score += difficulty_score * 0.1
321
+
322
+ return min(score, 1.0)
323
+
324
+ def _match_difficulty(self, node_difficulty: str, user_experience: str) -> float:
325
+ """Match node difficulty to user experience"""
326
+ difficulty_map = {'easy': 0, 'medium': 1, 'hard': 2}
327
+ experience_map = {'beginner': 0, 'intermediate': 1, 'advanced': 2}
328
+
329
+ node_level = difficulty_map.get(node_difficulty, 1)
330
+ user_level = experience_map.get(user_experience, 1)
331
+
332
+ # Prefer nodes at or slightly above user level
333
+ diff = abs(node_level - user_level)
334
+ if diff == 0:
335
+ return 1.0
336
+ elif diff == 1:
337
+ return 0.7
338
+ else:
339
+ return 0.3
340
+
341
+ def _prepare_knowledge_response(self,
342
+ ranked_results: List[Tuple[str, float]],
343
+ query: str,
344
+ user_profile: Optional[Dict[str, Any]]) -> Dict[str, Any]:
345
+ """Prepare structured knowledge response"""
346
+ response = {
347
+ 'query': query,
348
+ 'results': [],
349
+ 'topics': set(),
350
+ 'total_results': len(ranked_results),
351
+ 'timestamp': datetime.now().isoformat()
352
+ }
353
+
354
+ # Get top results
355
+ for node_id, relevance in ranked_results[:10]: # Top 10 results
356
+ node = self.knowledge_graph.get(node_id)
357
+ if node:
358
+ node_data = {
359
+ 'id': node.id,
360
+ 'type': node.type.value,
361
+ 'content': node.content,
362
+ 'topics': node.topics,
363
+ 'difficulty': node.difficulty,
364
+ 'relevance': relevance,
365
+ 'metadata': node.metadata,
366
+ 'access_count': node.access_count
367
+ }
368
+ response['results'].append(node_data)
369
+ response['topics'].update(node.topics)
370
+
371
+ response['topics'] = list(response['topics'])
372
+
373
+ # Add learning path if user profile provided
374
+ if user_profile and response['results']:
375
+ response['learning_path'] = self._generate_learning_path(
376
+ response['results'], user_profile
377
+ )
378
+
379
+ return response
380
+
381
+ def _generate_learning_path(self,
382
+ results: List[Dict[str, Any]],
383
+ user_profile: Dict[str, Any]) -> Dict[str, Any]:
384
+ """Generate learning path based on results and user profile"""
385
+ # Group by topic
386
+ topics = {}
387
+ for result in results:
388
+ for topic in result['topics']:
389
+ if topic not in topics:
390
+ topics[topic] = []
391
+ topics[topic].append(result)
392
+
393
+ # Select primary topic
394
+ primary_topic = max(topics.items(), key=lambda x: len(x[1]))[0] if topics else None
395
+
396
+ if not primary_topic:
397
+ return {'suggested_path': [], 'estimated_time': 0}
398
+
399
+ # Generate learning path
400
+ learning_path = []
401
+
402
+ # Start with concepts
403
+ concepts = [r for r in results if r['type'] == 'concept']
404
+ for concept in concepts[:3]:
405
+ learning_path.append({
406
+ 'type': 'concept',
407
+ 'title': concept['content'].get('name', 'Concept'),
408
+ 'time': 15, # minutes
409
+ 'description': f"Learn: {concept['content'].get('definition', '')[:100]}..."
410
+ })
411
+
412
+ # Add formulas
413
+ formulas = [r for r in results if r['type'] == 'formula']
414
+ for formula in formulas[:2]:
415
+ learning_path.append({
416
+ 'type': 'formula',
417
+ 'title': formula['content'].get('name', 'Formula'),
418
+ 'time': 10,
419
+ 'description': f"Master: {formula['content'].get('formula', '')}"
420
+ })
421
+
422
+ # Add practice
423
+ learning_path.append({
424
+ 'type': 'practice',
425
+ 'title': f'Practice {primary_topic} Problems',
426
+ 'time': 20,
427
+ 'description': 'Apply concepts through practice problems'
428
+ })
429
+
430
+ estimated_time = sum(step['time'] for step in learning_path)
431
+
432
+ return {
433
+ 'primary_topic': primary_topic,
434
+ 'suggested_path': learning_path,
435
+ 'estimated_time': estimated_time,
436
+ 'suitable_for': user_profile.get('experience_level', 'beginner')
437
+ }
438
+
439
+ def add_knowledge(self,
440
+ knowledge_type: KnowledgeType,
441
+ content: Dict[str, Any],
442
+ topics: List[str],
443
+ difficulty: str = 'medium',
444
+ metadata: Optional[Dict[str, Any]] = None) -> str:
445
+ """
446
+ Add new knowledge to the graph
447
+
448
+ Args:
449
+ knowledge_type: Type of knowledge
450
+ content: Knowledge content
451
+ topics: Related topics
452
+ difficulty: Difficulty level
453
+ metadata: Additional metadata
454
+
455
+ Returns:
456
+ Node ID
457
+ """
458
+ # Generate node
459
+ node_name = content.get('name', 'Unnamed Knowledge')
460
+ node_id = self._generate_node_id(node_name, knowledge_type)
461
+
462
+ node = KnowledgeNode(
463
+ id=node_id,
464
+ type=knowledge_type,
465
+ content=content,
466
+ topics=topics,
467
+ difficulty=difficulty,
468
+ prerequisites=[],
469
+ related_nodes=[],
470
+ metadata=metadata or {},
471
+ created_at=datetime.now(),
472
+ last_accessed=datetime.now(),
473
+ access_count=0
474
+ )
475
+
476
+ # Add to graph
477
+ self.knowledge_graph[node_id] = node
478
+
479
+ # Update indices
480
+ for topic in topics:
481
+ if topic not in self.topic_index:
482
+ self.topic_index[topic] = []
483
+ self.topic_index[topic].append(node_id)
484
+
485
+ # Index content keywords
486
+ content_str = str(content).lower()
487
+ for word in content_str.split():
488
+ if len(word) > 3:
489
+ if word not in self.concept_index:
490
+ self.concept_index[word] = []
491
+ self.concept_index[word].append(node_id)
492
+
493
+ print(f"✅ Added new knowledge: {node_name} ({node_id})")
494
+ return node_id
495
+
496
+ def get_knowledge_stats(self) -> Dict[str, Any]:
497
+ """Get knowledge engine statistics"""
498
+ stats = {
499
+ 'total_nodes': len(self.knowledge_graph),
500
+ 'by_type': {},
501
+ 'by_topic': {},
502
+ 'by_difficulty': {},
503
+ 'access_stats': {
504
+ 'total_accesses': sum(node.access_count for node in self.knowledge_graph.values()),
505
+ 'most_accessed': []
506
+ }
507
+ }
508
+
509
+ # Count by type
510
+ for node in self.knowledge_graph.values():
511
+ ktype = node.type.value
512
+ stats['by_type'][ktype] = stats['by_type'].get(ktype, 0) + 1
513
+
514
+ # Count by topic
515
+ for topic in node.topics:
516
+ stats['by_topic'][topic] = stats['by_topic'].get(topic, 0) + 1
517
+
518
+ # Count by difficulty
519
+ stats['by_difficulty'][node.difficulty] = stats['by_difficulty'].get(node.difficulty, 0) + 1
520
+
521
+ # Most accessed nodes
522
+ sorted_nodes = sorted(self.knowledge_graph.values(),
523
+ key=lambda x: x.access_count,
524
+ reverse=True)[:5]
525
+ stats['access_stats']['most_accessed'] = [
526
+ {
527
+ 'id': node.id,
528
+ 'name': node.content.get('name', 'Unnamed'),
529
+ 'access_count': node.access_count,
530
+ 'type': node.type.value
531
+ }
532
+ for node in sorted_nodes
533
+ ]
534
+
535
+ return stats
536
+
537
+ def export_knowledge_base(self, path: str):
538
+ """Export knowledge base to file"""
539
+ export_data = {
540
+ 'metadata': {
541
+ 'export_date': datetime.now().isoformat(),
542
+ 'total_nodes': len(self.knowledge_graph),
543
+ 'version': '1.0'
544
+ },
545
+ 'nodes': []
546
+ }
547
+
548
+ for node in self.knowledge_graph.values():
549
+ node_data = {
550
+ 'id': node.id,
551
+ 'type': node.type.value,
552
+ 'content': node.content,
553
+ 'topics': node.topics,
554
+ 'difficulty': node.difficulty,
555
+ 'metadata': node.metadata,
556
+ 'access_count': node.access_count,
557
+ 'created_at': node.created_at.isoformat(),
558
+ 'last_accessed': node.last_accessed.isoformat()
559
+ }
560
+ export_data['nodes'].append(node_data)
561
+
562
+ with open(path, 'w', encoding='utf-8') as f:
563
+ json.dump(export_data, f, indent=2, ensure_ascii=False)
564
+
565
+ print(f"✅ Knowledge base exported to {path}")