File size: 9,800 Bytes
bb8f662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Knowledge Graph Service for Neuro-Symbolic VQA
Uses ConceptNet API to provide common-sense reasoning capabilities
"""

import requests
import re
from typing import Dict, List, Optional
from functools import lru_cache
import time


class KnowledgeGraphService:
    """
    Lightweight ConceptNet API wrapper for common-sense reasoning.
    Enhances VQA answers with external knowledge about object properties,
    capabilities, uses, and relationships.
    """
    
    CONCEPTNET_API = "https://api.conceptnet.io"
    
    # Common-sense question patterns
    COMMONSENSE_PATTERNS = [
        # Capability questions
        (r'can .* (melt|freeze|fly|swim|float|sink|break|burn|explode)', 'CapableOf'),
        (r'is .* able to', 'CapableOf'),
        (r'does .* (float|sink)', 'CapableOf'),
        
        # Property questions
        (r'is .* (edible|poisonous|dangerous|safe|hot|cold|sweet|sour)', 'HasProperty'),
        (r'is this (food|drink|toy|tool|weapon)', 'HasProperty'),
        
        # Purpose questions
        (r'what .* (used for|for)', 'UsedFor'),
        (r'why .* (used|made)', 'UsedFor'),
        (r'how .* use', 'UsedFor'),
        
        # Composition questions
        (r'what .* made (of|from)', 'MadeOf'),
        (r'what .* (material|ingredient)', 'MadeOf'),
        
        # Location questions
        (r'where .* (found|located|kept|stored)', 'AtLocation'),
        (r'where (do|does) .* (live|grow)', 'AtLocation'),
    ]
    
    def __init__(self, cache_size=100, timeout=5):
        """
        Initialize Knowledge Graph service.
        
        Args:
            cache_size: Number of API responses to cache
            timeout: API request timeout in seconds
        """
        self.timeout = timeout
        self.cache_size = cache_size
        print("✅ Knowledge Graph service initialized (ConceptNet API)")
    
    @lru_cache(maxsize=100)
    def _query_conceptnet(self, concept: str, relation: str, limit: int = 10) -> Optional[Dict]:
        """
        Query ConceptNet API with caching.
        
        Args:
            concept: Concept to query (e.g., "ice_cream")
            relation: Relation type (e.g., "CapableOf", "HasProperty")
            limit: Maximum number of results
            
        Returns:
            API response dict or None if failed
        """
        try:
            # Normalize concept (replace spaces with underscores)
            concept = concept.lower().replace(' ', '_')
            
            # Build API URL
            url = f"{self.CONCEPTNET_API}/query"
            params = {
                'start': f'/c/en/{concept}',
                'rel': f'/r/{relation}',
                'limit': limit
            }
            
            # Make request
            response = requests.get(url, params=params, timeout=self.timeout)
            response.raise_for_status()
            
            return response.json()
            
        except requests.exceptions.Timeout:
            print(f"⚠️  ConceptNet API timeout for {concept}")
            return None
        except requests.exceptions.RequestException as e:
            print(f"⚠️  ConceptNet API error: {e}")
            return None
        except Exception as e:
            print(f"⚠️  Unexpected error querying ConceptNet: {e}")
            return None
    
    def get_concept_properties(self, concept: str) -> Dict[str, List[str]]:

        properties = {
            'CapableOf': [],
            'HasProperty': [],
            'UsedFor': [],
            'MadeOf': [],
            'AtLocation': []
        }
        
        # Query each relation type
        for relation in properties.keys():
            data = self._query_conceptnet(concept, relation)
            
            if data and 'edges' in data:
                for edge in data['edges']:
                    # Extract the end concept
                    if 'end' in edge and 'label' in edge['end']:
                        end_label = edge['end']['label']
                        properties[relation].append(end_label)
        
        return properties
    
    def is_commonsense_question(self, question: str) -> bool:
        """
        Detect if a question requires common-sense reasoning.
        
        Args:
            question: Question string
            
        Returns:
            True if question needs external knowledge
        """
        q_lower = question.lower()
        
        for pattern, _ in self.COMMONSENSE_PATTERNS:
            if re.search(pattern, q_lower):
                return True
        
        return False
    
    def _detect_question_type(self, question: str) -> Optional[str]:
        """
        Detect which ConceptNet relation the question is asking about.
        
        Args:
            question: Question string
            
        Returns:
            Relation type or None
        """
        q_lower = question.lower()
        
        for pattern, relation in self.COMMONSENSE_PATTERNS:
            if re.search(pattern, q_lower):
                return relation
        
        return None
    
    def answer_commonsense_question(self, object_name: str, question: str) -> Optional[str]:
        """
        Answer a common-sense question using Knowledge Graph.
        
        Args:
            object_name: Object detected by VQA (e.g., "ice cream")
            question: User's question
            
        Returns:
            Enhanced answer string or None
        """
        # Detect question type
        relation = self._detect_question_type(question)
        if not relation:
            return None
        
        # Query ConceptNet
        data = self._query_conceptnet(object_name, relation, limit=5)
        if not data or 'edges' not in data:
            return None
        
        # Extract relevant knowledge
        knowledge = []
        for edge in data['edges']:
            if 'end' in edge and 'label' in edge['end']:
                knowledge.append(edge['end']['label'])
        
        if not knowledge:
            return None
        
        # Generate natural language answer based on question type
        return self._synthesize_answer(object_name, question, relation, knowledge)
    
    def _synthesize_answer(self, object_name: str, question: str, 
                          relation: str, knowledge: List[str]) -> str:
        """
        Synthesize natural language answer from knowledge.
        
        Args:
            object_name: Detected object
            question: Original question
            relation: Relation type
            knowledge: List of related concepts from KG
            
        Returns:
            Natural language answer
        """
        q_lower = question.lower()
        
        # Capability questions (can X do Y?)
        if relation == 'CapableOf':
            # Check if specific capability is mentioned
            for capability in knowledge:
                if capability in q_lower:
                    return f"Yes, {object_name} can {capability}."
            
            # General capability answer
            if knowledge:
                caps = ', '.join(knowledge[:3])
                return f"{object_name.capitalize()} can {caps}."
        
        # Property questions (is X Y?)
        elif relation == 'HasProperty':
            # Check for specific property
            if 'edible' in q_lower:
                if 'edible' in knowledge:
                    return f"Yes, {object_name} is edible."
                else:
                    return f"No, {object_name} is not edible."
            
            if 'dangerous' in q_lower or 'safe' in q_lower:
                if any(prop in knowledge for prop in ['dangerous', 'harmful', 'poisonous']):
                    return f"Caution: {object_name} may be dangerous."
                else:
                    return f"{object_name.capitalize()} is generally safe."
            
            # General properties
            if knowledge:
                props = ', '.join(knowledge[:3])
                return f"{object_name.capitalize()} is {props}."
        
        # Purpose questions (what is X used for?)
        elif relation == 'UsedFor':
            if knowledge:
                uses = ', '.join(knowledge[:3])
                return f"{object_name.capitalize()} is used for {uses}."
        
        # Composition questions (what is X made of?)
        elif relation == 'MadeOf':
            if knowledge:
                materials = ', '.join(knowledge[:3])
                return f"{object_name.capitalize()} is made of {materials}."
        
        # Location questions (where is X found?)
        elif relation == 'AtLocation':
            if knowledge:
                locations = ', '.join(knowledge[:2])
                return f"{object_name.capitalize()} is typically found at {locations}."
        
        return None


# Test function
if __name__ == "__main__":
    print("=" * 80)
    print("🧪 Testing Knowledge Graph Service")
    print("=" * 80)
    
    kg = KnowledgeGraphService()
    
    # Test cases
    test_cases = [
        ("ice cream", "Can this melt?"),
        ("apple", "Is this edible?"),
        ("hammer", "What is this used for?"),
        ("knife", "Is this dangerous?"),
        ("bread", "What is this made of?"),
    ]
    
    for obj, question in test_cases:
        print(f"\n📝 Object: {obj}")
        print(f"❓ Question: {question}")
        
        # Check if common-sense question
        is_cs = kg.is_commonsense_question(question)
        print(f"🔍 Common-sense: {is_cs}")
        
        if is_cs:
            # Get answer
            answer = kg.answer_commonsense_question(obj, question)
            print(f"💬 Answer: {answer}")
        
        print("-" * 80)