File size: 26,040 Bytes
44c4559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
"""
Knowledge Agent - Retrieves and explains CFA concepts using local embeddings
Free version using sentence-transformers and ChromaDB
"""

import json
from typing import Dict, List, Any, Optional, Tuple
from datetime import datetime
import re
import numpy as np
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
import os


class KnowledgeAgent:
    """Retrieves and explains CFA knowledge concepts using local embeddings"""
    
    def __init__(self, knowledge_base_path: str = "data/cfa_knowledge_base.json"):
        self.knowledge_base_path = knowledge_base_path
        self.knowledge_base = self._load_knowledge_base()
        self.search_history = []
        
        # Initialize embedding model (free, runs locally)
        try:
            self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
            print("โœ… Sentence transformer model loaded")
        except Exception as e:
            print(f"โš ๏ธ Could not load sentence transformer: {e}")
            self.embedding_model = None
        
        # Initialize vector database
        self.chroma_client = None
        self.collection = None
        self._initialize_vector_db()
        
    def _load_knowledge_base(self) -> Dict[str, Any]:
        """Load CFA knowledge base from JSON file"""
        try:
            # Try multiple possible paths
            possible_paths = [
                self.knowledge_base_path,
                os.path.join(os.path.dirname(__file__), '..', self.knowledge_base_path),
                os.path.join(os.getcwd(), self.knowledge_base_path)
            ]
            
            for path in possible_paths:
                if os.path.exists(path):
                    with open(path, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                        print(f"โœ… Knowledge base loaded from {path}")
                        return data
            
            print("โš ๏ธ Knowledge base file not found, creating default")
            return self._create_default_knowledge_base()
            
        except Exception as e:
            print(f"โŒ Error loading knowledge base: {e}")
            return self._create_default_knowledge_base()
    
    def _create_default_knowledge_base(self) -> Dict[str, Any]:
        """Create a default knowledge base if file not found"""
        return {
            "topics": {
                "Time Value of Money": {
                    "description": "Core concept of finance dealing with the value of money over time",
                    "key_concepts": [
                        {
                            "name": "Present Value",
                            "definition": "Current worth of a future sum of money given a specified rate of return",
                            "formula": "PV = FV / (1 + r)^n",
                            "example": "The present value of $110 received one year from now at 10% discount rate is $100"
                        },
                        {
                            "name": "Future Value",
                            "definition": "Value of a current asset at a specified date in the future",
                            "formula": "FV = PV ร— (1 + r)^n",
                            "example": "$100 invested today at 10% annual interest will grow to $110 in one year"
                        }
                    ],
                    "common_applications": [
                        "Valuing bonds and stocks",
                        "Capital budgeting decisions",
                        "Retirement planning",
                        "Loan amortization"
                    ],
                    "cfa_level": "I",
                    "weight": "8-12%",
                    "difficulty": "Medium"
                },
                "Statistics": {
                    "description": "Collection, analysis, interpretation, and presentation of data",
                    "key_concepts": [
                        {
                            "name": "Mean",
                            "definition": "Average of a set of numbers",
                            "formula": "ฮผ = ฮฃ x_i / n",
                            "example": "Mean of [2, 4, 6, 8] is (2+4+6+8)/4 = 5"
                        },
                        {
                            "name": "Standard Deviation",
                            "definition": "Measure of the amount of variation or dispersion",
                            "formula": "ฯƒ = โˆš[ฮฃ(x_i - ฮผ)ยฒ / (n-1)]",
                            "example": "Standard deviation measures how spread out numbers are"
                        }
                    ],
                    "common_applications": [
                        "Portfolio analysis",
                        "Risk assessment",
                        "Performance measurement"
                    ],
                    "cfa_level": "I",
                    "weight": "5-8%",
                    "difficulty": "Medium"
                }
            },
            "formulas": {
                "TVM": [
                    {"name": "Future Value", "formula": "FV = PV(1+r)^n"},
                    {"name": "Present Value", "formula": "PV = FV/(1+r)^n"}
                ]
            },
            "glossary": {
                "NPV": "Net Present Value - difference between present value of cash inflows and outflows",
                "IRR": "Internal Rate of Return - discount rate that makes NPV zero",
                "Beta": "Measure of a stock's volatility relative to the market",
                "Alpha": "Measure of performance on a risk-adjusted basis"
            }
        }
    
    def _initialize_vector_db(self):
        """Initialize ChromaDB vector database"""
        try:
            # Create persistent directory for ChromaDB
            chroma_dir = os.path.join(os.path.dirname(__file__), '..', 'chroma_db')
            os.makedirs(chroma_dir, exist_ok=True)
            
            self.chroma_client = chromadb.PersistentClient(
                path=chroma_dir,
                settings=Settings(anonymized_telemetry=False)
            )
            
            # Create or get collection
            self.collection = self.chroma_client.get_or_create_collection(
                name="cfa_knowledge",
                metadata={"description": "CFA Knowledge Base Embeddings"}
            )
            
            print("โœ… ChromaDB initialized")
            
            # Index knowledge if collection is empty
            if self.collection.count() == 0:
                self._index_knowledge_base()
                
        except Exception as e:
            print(f"โš ๏ธ Could not initialize ChromaDB: {e}")
            self.chroma_client = None
            self.collection = None
    
    def _index_knowledge_base(self):
        """Index knowledge base in vector database"""
        if not self.embedding_model or not self.collection:
            return
        
        print("๐Ÿ“š Indexing knowledge base...")
        
        documents = []
        metadatas = []
        ids = []
        
        # Index topics
        for topic_name, topic_data in self.knowledge_base.get('topics', {}).items():
            # Topic description
            doc_text = f"Topic: {topic_name}. Description: {topic_data.get('description', '')}"
            documents.append(doc_text)
            metadatas.append({
                "type": "topic",
                "topic": topic_name,
                "cfa_level": topic_data.get('cfa_level', 'I'),
                "difficulty": topic_data.get('difficulty', 'Medium')
            })
            ids.append(f"topic_{topic_name}")
            
            # Key concepts
            for concept in topic_data.get('key_concepts', []):
                doc_text = f"Concept: {concept['name']}. Definition: {concept.get('definition', '')}"
                if 'formula' in concept:
                    doc_text += f" Formula: {concept['formula']}"
                if 'example' in concept:
                    doc_text += f" Example: {concept['example']}"
                
                documents.append(doc_text)
                metadatas.append({
                    "type": "concept",
                    "topic": topic_name,
                    "concept": concept['name'],
                    "has_formula": 'formula' in concept
                })
                ids.append(f"concept_{topic_name}_{concept['name']}")
        
        # Index formulas
        for category, formulas in self.knowledge_base.get('formulas', {}).items():
            for formula in formulas:
                doc_text = f"Formula: {formula['name']}. Formula: {formula.get('formula', '')}"
                if 'description' in formula:
                    doc_text += f" Description: {formula['description']}"
                
                documents.append(doc_text)
                metadatas.append({
                    "type": "formula",
                    "category": category,
                    "formula_name": formula['name']
                })
                ids.append(f"formula_{category}_{formula['name']}")
        
        # Index glossary terms
        for term, definition in self.knowledge_base.get('glossary', {}).items():
            doc_text = f"Term: {term}. Definition: {definition}"
            documents.append(doc_text)
            metadatas.append({
                "type": "glossary",
                "term": term
            })
            ids.append(f"glossary_{term}")
        
        # Generate embeddings and add to collection
        if documents:
            try:
                embeddings = self.embedding_model.encode(documents).tolist()
                self.collection.add(
                    documents=documents,
                    metadatas=metadatas,
                    ids=ids,
                    embeddings=embeddings
                )
                print(f"โœ… Indexed {len(documents)} knowledge items")
            except Exception as e:
                print(f"โŒ Error indexing documents: {e}")
    
    def process(self,
                query: str,
                user_profile: Dict[str, Any],
                conversation_history: List[Dict] = None) -> Dict[str, Any]:
        """
        Retrieve and explain CFA knowledge concepts
        
        Args:
            query: Knowledge query
            user_profile: User's learning profile
            conversation_history: Previous conversation
        
        Returns:
            Knowledge explanation
        """
        # Analyze query to determine knowledge needs
        knowledge_type = self._analyze_knowledge_query(query)
        
        # Search knowledge base
        if self.collection and self.embedding_model:
            # Use vector search
            search_results = self._vector_search(query, knowledge_type, user_profile)
        else:
            # Fallback to keyword search
            search_results = self._keyword_search(query, knowledge_type)
        
        # Format response
        response = self._format_knowledge_response(query, search_results, knowledge_type, user_profile)
        
        # Log search
        self._log_search(query, knowledge_type, search_results)
        
        return {
            'response': response,
            'knowledge_type': knowledge_type,
            'search_results': search_results,
            'confidence': self._calculate_confidence(search_results),
            'metadata': {
                'search_method': 'vector' if self.collection else 'keyword',
                'result_count': len(search_results.get('items', []))
            }
        }
    
    def _analyze_knowledge_query(self, query: str) -> str:
        """Analyze query to determine type of knowledge needed"""
        query_lower = query.lower()
        
        if any(term in query_lower for term in ['what is', 'define', 'definition of', 'meaning of']):
            return 'definition'
        elif any(term in query_lower for term in ['explain', 'how does', 'how to', 'understand']):
            return 'explanation'
        elif any(term in query_lower for term in ['formula', 'calculate', 'equation', 'solve']):
            return 'formula'
        elif any(term in query_lower for term in ['example', 'case study', 'application', 'real world']):
            return 'example'
        elif any(term in query_lower for term in ['compare', 'difference between', 'vs', 'versus']):
            return 'comparison'
        elif any(term in query_lower for term in ['list', 'what are the', 'types of', 'categories']):
            return 'list'
        else:
            return 'general'
    
    def _vector_search(self, query: str, knowledge_type: str, user_profile: Dict[str, Any]) -> Dict[str, Any]:
        """Search using vector embeddings"""
        results = {
            'items': [],
            'topics': set(),
            'method': 'vector'
        }
        
        try:
            # Generate query embedding
            query_embedding = self.embedding_model.encode(query).tolist()
            
            # Search collection
            search_results = self.collection.query(
                query_embeddings=[query_embedding],
                n_results=10,
                include=["documents", "metadatas", "distances"]
            )
            
            # Process results
            if search_results['documents']:
                for i, (doc, metadata, distance) in enumerate(zip(
                    search_results['documents'][0],
                    search_results['metadatas'][0],
                    search_results['distances'][0]
                )):
                    # Convert distance to similarity score (0-1)
                    similarity = 1 - (distance / 2)  # Rough approximation
                    
                    item = {
                        'content': doc,
                        'metadata': metadata,
                        'similarity': similarity,
                        'type': metadata.get('type', 'unknown')
                    }
                    
                    results['items'].append(item)
                    
                    # Extract topics
                    if 'topic' in metadata:
                        results['topics'].add(metadata['topic'])
            
            # Filter by knowledge type if needed
            if knowledge_type != 'general':
                filtered_items = []
                for item in results['items']:
                    if self._matches_knowledge_type(item, knowledge_type):
                        filtered_items.append(item)
                results['items'] = filtered_items[:5]  # Limit to 5
            
        except Exception as e:
            print(f"โŒ Vector search error: {e}")
            # Fall back to keyword search
            return self._keyword_search(query, knowledge_type)
        
        return results
    
    def _keyword_search(self, query: str, knowledge_type: str) -> Dict[str, Any]:
        """Fallback keyword search"""
        results = {
            'items': [],
            'topics': set(),
            'method': 'keyword'
        }
        
        query_lower = query.lower()
        query_terms = set(query_lower.split())
        
        # Search topics
        for topic_name, topic_data in self.knowledge_base.get('topics', {}).items():
            topic_lower = topic_name.lower()
            
            # Check topic name
            if any(term in topic_lower for term in query_terms):
                results['topics'].add(topic_name)
                results['items'].append({
                    'content': f"Topic: {topic_name}. {topic_data.get('description', '')}",
                    'metadata': {'type': 'topic', 'topic': topic_name},
                    'similarity': 0.8,
                    'type': 'topic'
                })
            
            # Search concepts within topic
            for concept in topic_data.get('key_concepts', []):
                concept_lower = concept['name'].lower()
                if any(term in concept_lower for term in query_terms):
                    results['topics'].add(topic_name)
                    content = f"Concept: {concept['name']}. {concept.get('definition', '')}"
                    if 'formula' in concept:
                        content += f" Formula: {concept['formula']}"
                    results['items'].append({
                        'content': content,
                        'metadata': {'type': 'concept', 'topic': topic_name, 'concept': concept['name']},
                        'similarity': 0.7,
                        'type': 'concept'
                    })
        
        # Search glossary
        for term, definition in self.knowledge_base.get('glossary', {}).items():
            term_lower = term.lower()
            if term_lower in query_lower or any(term_word in query_terms for term_word in term_lower.split()):
                results['items'].append({
                    'content': f"Term: {term}. Definition: {definition}",
                    'metadata': {'type': 'glossary', 'term': term},
                    'similarity': 0.9,
                    'type': 'glossary'
                })
        
        # Sort by similarity (keyword match quality)
        results['items'].sort(key=lambda x: x['similarity'], reverse=True)
        results['items'] = results['items'][:5]  # Limit to 5
        
        return results
    
    def _matches_knowledge_type(self, item: Dict[str, Any], knowledge_type: str) -> bool:
        """Check if item matches the requested knowledge type"""
        item_type = item.get('type')
        
        type_mapping = {
            'definition': ['glossary', 'concept'],
            'explanation': ['topic', 'concept'],
            'formula': ['concept', 'formula'],
            'example': ['concept'],
            'comparison': ['topic', 'concept'],
            'list': ['topic']
        }
        
        if knowledge_type in type_mapping:
            return item_type in type_mapping[knowledge_type]
        
        return True
    
    def _format_knowledge_response(self, 
                                 query: str, 
                                 results: Dict[str, Any], 
                                 knowledge_type: str,
                                 user_profile: Dict[str, Any]) -> str:
        """Format knowledge response based on query type"""
        
        response_parts = []
        
        if not results.get('items'):
            response_parts.append(f"I couldn't find specific information about '{query}'.")
            response_parts.append("\n**Try asking about:**")
            response_parts.append("- Time Value of Money (TVM)")
            response_parts.append("- Statistics and probability")
            response_parts.append("- Portfolio management")
            response_parts.append("- Financial ratios")
            response_parts.append("- CFA ethics")
            return "\n".join(response_parts)
        
        # Add header based on knowledge type
        headers = {
            'definition': "๐Ÿ“š Definitions",
            'explanation': "๐Ÿ“– Explanations",
            'formula': "๐Ÿงฎ Formulas",
            'example': "๐Ÿ’ก Examples",
            'comparison': "โš–๏ธ Comparisons",
            'list': "๐Ÿ“‹ Lists",
            'general': "๐Ÿ“š Knowledge Results"
        }
        
        response_parts.append(f"## {headers.get(knowledge_type, '๐Ÿ“š Knowledge')}")
        
        # Add results
        for i, item in enumerate(results['items'][:3], 1):  # Limit to 3 items
            response_parts.append(f"\n### Result {i}")
            
            content = item['content']
            metadata = item.get('metadata', {})
            
            # Format based on type
            if metadata.get('type') == 'topic':
                response_parts.append(f"**Topic:** {metadata.get('topic', 'Unknown')}")
                response_parts.append(f"{content}")
                
            elif metadata.get('type') == 'concept':
                response_parts.append(f"**Concept:** {metadata.get('concept', 'Unknown')}")
                response_parts.append(f"**Topic:** {metadata.get('topic', 'General')}")
                response_parts.append(f"{content}")
                
            elif metadata.get('type') == 'glossary':
                response_parts.append(f"**Term:** {metadata.get('term', 'Unknown')}")
                response_parts.append(f"{content}")
                
            elif metadata.get('type') == 'formula':
                response_parts.append(f"**Formula:** {metadata.get('formula_name', 'Unknown')}")
                response_parts.append(f"{content}")
            
            else:
                response_parts.append(f"{content}")
        
        # Add study tips if relevant
        if results.get('topics'):
            topics = list(results['topics'])[:2]  # Limit to 2 topics
            if topics:
                response_parts.append("\n---\n")
                response_parts.append("**๐Ÿ“ Study Tips:**")
                
                for topic in topics:
                    tips = self._get_study_tips(topic, user_profile)
                    if tips:
                        response_parts.append(f"\n**For {topic}:**")
                        for tip in tips[:2]:
                            response_parts.append(f"- {tip}")
        
        return "\n".join(response_parts)
    
    def _get_study_tips(self, topic: str, user_profile: Dict[str, Any]) -> List[str]:
        """Get study tips for a topic"""
        tips_db = {
            'Time Value of Money': [
                "Practice with different compounding periods (annual, semi-annual, monthly)",
                "Master the TVM calculator keys: N, I/Y, PV, PMT, FV",
                "Understand the relationship between discount rate and present value"
            ],
            'Statistics': [
                "Focus on interpreting results, not just calculations",
                "Understand when to use population vs sample statistics",
                "Practice hypothesis testing steps systematically"
            ],
            'Portfolio Management': [
                "Memorize the CAPM formula: E(R) = Rf + ฮฒ(Rm - Rf)",
                "Understand the efficient frontier and capital market line",
                "Practice calculating portfolio variance with different correlations"
            ]
        }
        
        tips = tips_db.get(topic, [
            "Break down complex concepts into smaller parts",
            "Create flashcards for key terms and formulas",
            "Practice with real-world examples"
        ])
        
        # Adjust based on user profile
        experience = user_profile.get('experience_level', 'beginner')
        if experience == 'beginner':
            tips.insert(0, "Start with foundational concepts before advancing")
        elif experience == 'advanced':
            tips.append("Focus on application and analysis questions")
        
        return tips[:3]  # Return top 3 tips
    
    def _calculate_confidence(self, results: Dict[str, Any]) -> float:
        """Calculate confidence in search results"""
        if not results.get('items'):
            return 0.2
        
        # Calculate average similarity
        similarities = [item.get('similarity', 0.5) for item in results['items']]
        avg_similarity = sum(similarities) / len(similarities)
        
        # Adjust based on result count
        count_factor = min(len(results['items']) / 5, 1.0) * 0.2
        
        confidence = avg_similarity + count_factor
        return min(confidence, 0.95)
    
    def _log_search(self, query: str, knowledge_type: str, results: Dict[str, Any]):
        """Log knowledge searches"""
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'query': query,
            'knowledge_type': knowledge_type,
            'result_count': len(results.get('items', [])),
            'method': results.get('method', 'unknown'),
            'topics': list(results.get('topics', set()))
        }
        self.search_history.append(log_entry)
        
        # Keep only last 100 searches
        if len(self.search_history) > 100:
            self.search_history = self.search_history[-100:]
    
    def get_search_history(self, limit: int = 10) -> List[Dict]:
        """Get recent search history"""
        return self.search_history[-limit:]
    
    def get_metrics(self) -> Dict[str, Any]:
        """Get knowledge agent metrics"""
        if not self.search_history:
            return {
                'total_searches': 0,
                'vector_db_ready': self.collection is not None,
                'embedding_model_ready': self.embedding_model is not None,
                'knowledge_items': self.collection.count() if self.collection else 0
            }
        
        # Calculate success rate (searches with results)
        successful = sum(1 for entry in self.search_history if entry['result_count'] > 0)
        success_rate = successful / len(self.search_history) if self.search_history else 0
        
        # Get most searched topics
        topic_counts = {}
        for entry in self.search_history:
            for topic in entry.get('topics', []):
                topic_counts[topic] = topic_counts.get(topic, 0) + 1
        
        common_topics = sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)[:3]
        
        return {
            'total_searches': len(self.search_history),
            'success_rate': success_rate,
            'common_topics': common_topics,
            'vector_db_ready': self.collection is not None,
            'embedding_model_ready': self.embedding_model is not None,
            'knowledge_items': self.collection.count() if self.collection else 0,
            'recent_queries': [entry['query'][:50] for entry in self.search_history[-5:]]
        }