KrishnaCosmic commited on
Commit
3fd898e
·
1 Parent(s): 8ff58c6

finalising

Browse files
Files changed (3) hide show
  1. config/redis.py +225 -0
  2. main.py +50 -7
  3. requirements.txt +4 -1
config/redis.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Redis Connection Utility
3
+
4
+ Provides Redis client with connection pooling and helper methods for caching.
5
+ Falls back gracefully when Redis is unavailable.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+ import hashlib
11
+ import json
12
+ from typing import Optional, Any
13
+ from redis import Redis, ConnectionPool, RedisError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Redis configuration from environment
18
+ REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
19
+ DEFAULT_TTL = 86400 # 24 hours in seconds
20
+
21
+ # Global connection pool
22
+ _redis_pool: Optional[ConnectionPool] = None
23
+ _redis_client: Optional[Redis] = None
24
+
25
+
26
+ def get_redis_client() -> Optional[Redis]:
27
+ """
28
+ Get Redis client with connection pooling.
29
+ Returns None if Redis is unavailable.
30
+ """
31
+ global _redis_pool, _redis_client
32
+
33
+ if _redis_client is not None:
34
+ return _redis_client
35
+
36
+ try:
37
+ if _redis_pool is None:
38
+ _redis_pool = ConnectionPool.from_url(
39
+ REDIS_URL,
40
+ max_connections=10,
41
+ decode_responses=True,
42
+ socket_timeout=5,
43
+ socket_connect_timeout=5
44
+ )
45
+
46
+ _redis_client = Redis(connection_pool=_redis_pool)
47
+
48
+ # Test connection
49
+ _redis_client.ping()
50
+ logger.info(f"Redis connected successfully at {REDIS_URL}")
51
+ return _redis_client
52
+
53
+ except RedisError as e:
54
+ logger.warning(f"Redis connection failed: {e}. Caching will be disabled.")
55
+ return None
56
+ except Exception as e:
57
+ logger.error(f"Unexpected error connecting to Redis: {e}")
58
+ return None
59
+
60
+
61
+ def generate_cache_key(prefix: str, data: dict) -> str:
62
+ """
63
+ Generate a consistent cache key from a data dictionary.
64
+
65
+ Args:
66
+ prefix: Key prefix (e.g., "triage", "prediction")
67
+ data: Dictionary containing the data to hash
68
+
69
+ Returns:
70
+ Cache key string
71
+ """
72
+ # Create a stable JSON representation
73
+ json_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
74
+
75
+ # Generate SHA256 hash
76
+ hash_obj = hashlib.sha256(json_str.encode('utf-8'))
77
+ hash_hex = hash_obj.hexdigest()[:16] # Use first 16 chars
78
+
79
+ return f"{prefix}:{hash_hex}"
80
+
81
+
82
+ def cache_get(key: str) -> Optional[Any]:
83
+ """
84
+ Get value from Redis cache.
85
+
86
+ Args:
87
+ key: Cache key
88
+
89
+ Returns:
90
+ Cached value (parsed from JSON) or None if not found or error
91
+ """
92
+ client = get_redis_client()
93
+ if client is None:
94
+ return None
95
+
96
+ try:
97
+ value = client.get(key)
98
+ if value is None:
99
+ return None
100
+
101
+ # Parse JSON
102
+ return json.loads(value)
103
+
104
+ except RedisError as e:
105
+ logger.warning(f"Redis get error for key '{key}': {e}")
106
+ return None
107
+ except json.JSONDecodeError as e:
108
+ logger.error(f"Failed to decode cached value for key '{key}': {e}")
109
+ return None
110
+ except Exception as e:
111
+ logger.error(f"Unexpected error getting cache key '{key}': {e}")
112
+ return None
113
+
114
+
115
+ def cache_set(key: str, value: Any, ttl: int = DEFAULT_TTL) -> bool:
116
+ """
117
+ Set value in Redis cache with TTL.
118
+
119
+ Args:
120
+ key: Cache key
121
+ value: Value to cache (will be JSON serialized)
122
+ ttl: Time to live in seconds (default: 24 hours)
123
+
124
+ Returns:
125
+ True if successful, False otherwise
126
+ """
127
+ client = get_redis_client()
128
+ if client is None:
129
+ return False
130
+
131
+ try:
132
+ # Serialize to JSON
133
+ json_value = json.dumps(value)
134
+
135
+ # Set with expiration
136
+ client.setex(key, ttl, json_value)
137
+ logger.debug(f"Cached key '{key}' with TTL {ttl}s")
138
+ return True
139
+
140
+ except RedisError as e:
141
+ logger.warning(f"Redis set error for key '{key}': {e}")
142
+ return False
143
+ except (TypeError, ValueError) as e:
144
+ logger.error(f"Failed to serialize value for key '{key}': {e}")
145
+ return False
146
+ except Exception as e:
147
+ logger.error(f"Unexpected error setting cache key '{key}': {e}")
148
+ return False
149
+
150
+
151
+ def cache_delete(key: str) -> bool:
152
+ """
153
+ Delete value from Redis cache.
154
+
155
+ Args:
156
+ key: Cache key
157
+
158
+ Returns:
159
+ True if successful, False otherwise
160
+ """
161
+ client = get_redis_client()
162
+ if client is None:
163
+ return False
164
+
165
+ try:
166
+ client.delete(key)
167
+ logger.debug(f"Deleted cache key '{key}'")
168
+ return True
169
+
170
+ except RedisError as e:
171
+ logger.warning(f"Redis delete error for key '{key}': {e}")
172
+ return False
173
+ except Exception as e:
174
+ logger.error(f"Unexpected error deleting cache key '{key}': {e}")
175
+ return False
176
+
177
+
178
+ def cache_exists(key: str) -> bool:
179
+ """
180
+ Check if key exists in Redis cache.
181
+
182
+ Args:
183
+ key: Cache key
184
+
185
+ Returns:
186
+ True if key exists, False otherwise
187
+ """
188
+ client = get_redis_client()
189
+ if client is None:
190
+ return False
191
+
192
+ try:
193
+ return client.exists(key) > 0
194
+
195
+ except RedisError as e:
196
+ logger.warning(f"Redis exists error for key '{key}': {e}")
197
+ return False
198
+ except Exception as e:
199
+ logger.error(f"Unexpected error checking cache key '{key}': {e}")
200
+ return False
201
+
202
+
203
+ def get_cache_stats() -> dict:
204
+ """
205
+ Get Redis cache statistics.
206
+
207
+ Returns:
208
+ Dictionary with cache stats or empty dict if unavailable
209
+ """
210
+ client = get_redis_client()
211
+ if client is None:
212
+ return {"status": "unavailable"}
213
+
214
+ try:
215
+ info = client.info("stats")
216
+ return {
217
+ "status": "connected",
218
+ "total_keys": client.dbsize(),
219
+ "hits": info.get("keyspace_hits", 0),
220
+ "misses": info.get("keyspace_misses", 0),
221
+ "evicted_keys": info.get("evicted_keys", 0),
222
+ }
223
+ except RedisError as e:
224
+ logger.warning(f"Failed to get cache stats: {e}")
225
+ return {"status": "error", "error": str(e)}
main.py CHANGED
@@ -10,8 +10,11 @@ Designed for Hugging Face Spaces deployment.
10
  import logging
11
  import os
12
  from contextlib import asynccontextmanager
13
- from fastapi import FastAPI, HTTPException, Request
14
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
15
  from pydantic import BaseModel
16
  from typing import List, Dict, Any, Optional
17
  from datetime import datetime, timezone
@@ -167,13 +170,38 @@ async def root():
167
  # =============================================================================
168
 
169
  @app.post("/triage")
170
- async def triage_issue(request: TriageRequest):
171
  """
172
  Classify and triage a GitHub issue using AI.
173
 
174
  Passes directly to ai_triage_service.classify_issue()
 
 
 
175
  """
176
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  # Create Issue object matching the original service expectation
178
  issue = Issue(
179
  id=request.id or "temp-id",
@@ -187,7 +215,17 @@ async def triage_issue(request: TriageRequest):
187
  isPR=request.isPR
188
  )
189
 
 
190
  result = await ai_triage_service.classify_issue(issue)
 
 
 
 
 
 
 
 
 
191
  return result
192
  except Exception as e:
193
  logger.error(f"Triage error: {e}")
@@ -199,11 +237,12 @@ async def triage_issue(request: TriageRequest):
199
  # =============================================================================
200
 
201
  @app.post("/chat")
202
- async def chat(request: ChatRequest):
203
  """
204
  AI chat endpoint for general assistance.
205
 
206
  Passes directly to ai_chat_service.chat()
 
207
  """
208
  try:
209
  response = await ai_chat_service.chat(
@@ -222,11 +261,12 @@ async def chat(request: ChatRequest):
222
  # =============================================================================
223
 
224
  @app.post("/rag/chat")
225
- async def rag_chat(request: RAGChatRequest):
226
  """
227
  Answer questions using RAG (Retrieval-Augmented Generation).
228
 
229
  Passes directly to rag_chatbot_service.answer_question()
 
230
  """
231
  try:
232
  result = await rag_chatbot_service.answer_question(
@@ -242,11 +282,12 @@ async def rag_chat(request: RAGChatRequest):
242
 
243
 
244
  @app.post("/rag/index")
245
- async def rag_index(request: RAGIndexRequest):
246
  """
247
  Index a repository for RAG search.
248
 
249
  Passes directly to rag_chatbot_service.index_repository()
 
250
  """
251
  try:
252
  result = await rag_chatbot_service.index_repository(
@@ -275,11 +316,12 @@ async def rag_suggestions(repo_name: Optional[str] = None):
275
  # =============================================================================
276
 
277
  @app.post("/mentor-match")
278
- async def mentor_match(request: MentorMatchRequest):
279
  """
280
  Find mentor matches for a user.
281
 
282
  Passes directly to mentor_matching_service.find_mentors_for_user()
 
283
  """
284
  try:
285
  matches = mentor_matching_service.find_mentors_for_user(
@@ -299,11 +341,12 @@ async def mentor_match(request: MentorMatchRequest):
299
  # =============================================================================
300
 
301
  @app.post("/hype")
302
- async def generate_hype(request: HypeRequest):
303
  """
304
  Generate hype/celebration message for a PR.
305
 
306
  Passes directly to hype_generator_service.generate_hype()
 
307
  """
308
  try:
309
  result = hype_generator_service.generate_hype(
 
10
  import logging
11
  import os
12
  from contextlib import asynccontextmanager
13
+ from fastapi import FastAPI, HTTPException, Request, Depends
14
  from fastapi.middleware.cors import CORSMiddleware
15
+
16
+ # Import authentication middleware
17
+ from middleware import require_api_key_or_auth, get_optional_user
18
  from pydantic import BaseModel
19
  from typing import List, Dict, Any, Optional
20
  from datetime import datetime, timezone
 
170
  # =============================================================================
171
 
172
  @app.post("/triage")
173
+ async def triage_issue(request: TriageRequest, auth: dict = Depends(require_api_key_or_auth)):
174
  """
175
  Classify and triage a GitHub issue using AI.
176
 
177
  Passes directly to ai_triage_service.classify_issue()
178
+ Requires authentication (API key or JWT token).
179
+
180
+ Implements Redis caching with 24-hour TTL.
181
  """
182
  try:
183
+ # Import Redis utilities (lazy import to avoid startup dependencies)
184
+ from config.redis import generate_cache_key, cache_get, cache_set
185
+
186
+ # Generate cache key from request data
187
+ cache_data = {
188
+ "title": request.title,
189
+ "body": request.body or "",
190
+ "isPR": request.isPR
191
+ }
192
+ cache_key = generate_cache_key("triage", cache_data)
193
+
194
+ # Check cache first
195
+ cached_result = cache_get(cache_key)
196
+ if cached_result is not None:
197
+ logger.info(f"Cache HIT for triage request: {cache_key}")
198
+ # Add cache metadata
199
+ cached_result["_cached"] = True
200
+ cached_result["_cache_key"] = cache_key
201
+ return cached_result
202
+
203
+ logger.info(f"Cache MISS for triage request: {cache_key}")
204
+
205
  # Create Issue object matching the original service expectation
206
  issue = Issue(
207
  id=request.id or "temp-id",
 
215
  isPR=request.isPR
216
  )
217
 
218
+ # Call AI service (cache miss)
219
  result = await ai_triage_service.classify_issue(issue)
220
+
221
+ # Cache the result with 24-hour TTL (86400 seconds)
222
+ cache_set(cache_key, result, ttl=86400)
223
+ logger.info(f"Cached triage result: {cache_key}")
224
+
225
+ # Add cache metadata
226
+ result["_cached"] = False
227
+ result["_cache_key"] = cache_key
228
+
229
  return result
230
  except Exception as e:
231
  logger.error(f"Triage error: {e}")
 
237
  # =============================================================================
238
 
239
  @app.post("/chat")
240
+ async def chat(request: ChatRequest, auth: dict = Depends(require_api_key_or_auth)):
241
  """
242
  AI chat endpoint for general assistance.
243
 
244
  Passes directly to ai_chat_service.chat()
245
+ Requires authentication (API key or JWT token).
246
  """
247
  try:
248
  response = await ai_chat_service.chat(
 
261
  # =============================================================================
262
 
263
  @app.post("/rag/chat")
264
+ async def rag_chat(request: RAGChatRequest, auth: dict = Depends(require_api_key_or_auth)):
265
  """
266
  Answer questions using RAG (Retrieval-Augmented Generation).
267
 
268
  Passes directly to rag_chatbot_service.answer_question()
269
+ Requires authentication.
270
  """
271
  try:
272
  result = await rag_chatbot_service.answer_question(
 
282
 
283
 
284
  @app.post("/rag/index")
285
+ async def rag_index(request: RAGIndexRequest, auth: dict = Depends(require_api_key_or_auth)):
286
  """
287
  Index a repository for RAG search.
288
 
289
  Passes directly to rag_chatbot_service.index_repository()
290
+ Requires authentication.
291
  """
292
  try:
293
  result = await rag_chatbot_service.index_repository(
 
316
  # =============================================================================
317
 
318
  @app.post("/mentor-match")
319
+ async def mentor_match(request: MentorMatchRequest, auth: dict = Depends(require_api_key_or_auth)):
320
  """
321
  Find mentor matches for a user.
322
 
323
  Passes directly to mentor_matching_service.find_mentors_for_user()
324
+ Requires authentication.
325
  """
326
  try:
327
  matches = mentor_matching_service.find_mentors_for_user(
 
341
  # =============================================================================
342
 
343
  @app.post("/hype")
344
+ async def generate_hype(request: HypeRequest, auth: dict = Depends(require_api_key_or_auth)):
345
  """
346
  Generate hype/celebration message for a PR.
347
 
348
  Passes directly to hype_generator_service.generate_hype()
349
+ Requires authentication.
350
  """
351
  try:
352
  result = hype_generator_service.generate_hype(
requirements.txt CHANGED
@@ -44,6 +44,9 @@ asyncio-throttle>=1.0.2
44
  # JWT Authentication
45
  PyJWT>=2.8.0
46
 
 
 
 
47
  # Turso (libsql) Database - try both package names
48
  libsql-experimental>=0.0.55
49
- libsql_client
 
44
  # JWT Authentication
45
  PyJWT>=2.8.0
46
 
47
+ # Redis Cache
48
+ redis>=5.0.0
49
+
50
  # Turso (libsql) Database - try both package names
51
  libsql-experimental>=0.0.55
52
+ libsql_client