empathy-framework 5.0.0__py3-none-any.whl → 5.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/METADATA +53 -9
  2. {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/RECORD +28 -31
  3. empathy_llm_toolkit/providers.py +175 -35
  4. empathy_llm_toolkit/utils/tokens.py +150 -30
  5. empathy_os/__init__.py +1 -1
  6. empathy_os/cli/commands/batch.py +256 -0
  7. empathy_os/cli/commands/cache.py +248 -0
  8. empathy_os/cli/commands/inspect.py +1 -2
  9. empathy_os/cli/commands/metrics.py +1 -1
  10. empathy_os/cli/commands/routing.py +285 -0
  11. empathy_os/cli/commands/workflow.py +2 -2
  12. empathy_os/cli/parsers/__init__.py +6 -0
  13. empathy_os/cli/parsers/batch.py +118 -0
  14. empathy_os/cli/parsers/cache.py +65 -0
  15. empathy_os/cli/parsers/routing.py +110 -0
  16. empathy_os/dashboard/standalone_server.py +22 -11
  17. empathy_os/metrics/collector.py +31 -0
  18. empathy_os/models/token_estimator.py +21 -13
  19. empathy_os/telemetry/agent_coordination.py +12 -14
  20. empathy_os/telemetry/agent_tracking.py +18 -19
  21. empathy_os/telemetry/approval_gates.py +27 -39
  22. empathy_os/telemetry/event_streaming.py +19 -19
  23. empathy_os/telemetry/feedback_loop.py +13 -16
  24. empathy_os/workflows/batch_processing.py +56 -10
  25. empathy_os/vscode_bridge 2.py +0 -173
  26. empathy_os/workflows/progressive/README 2.md +0 -454
  27. empathy_os/workflows/progressive/__init__ 2.py +0 -92
  28. empathy_os/workflows/progressive/cli 2.py +0 -242
  29. empathy_os/workflows/progressive/core 2.py +0 -488
  30. empathy_os/workflows/progressive/orchestrator 2.py +0 -701
  31. empathy_os/workflows/progressive/reports 2.py +0 -528
  32. empathy_os/workflows/progressive/telemetry 2.py +0 -280
  33. empathy_os/workflows/progressive/test_gen 2.py +0 -514
  34. empathy_os/workflows/progressive/workflow 2.py +0 -628
  35. {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/WHEEL +0 -0
  36. {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/entry_points.txt +0 -0
  37. {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/licenses/LICENSE +0 -0
  38. {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/top_level.txt +0 -0
@@ -236,19 +236,13 @@ class ApprovalGate:
236
236
  # Store approval request (for UI to retrieve)
237
237
  request_key = f"approval_request:{request_id}"
238
238
  try:
239
- if hasattr(self.memory, "stash"):
240
- self.memory.stash(
241
- key=request_key,
242
- data=request.to_dict(),
243
- credentials=None,
244
- ttl_seconds=int(timeout) + 60, # TTL = timeout + buffer
245
- )
246
- elif hasattr(self.memory, "_redis"):
239
+ # Use direct Redis access for custom TTL
240
+ if hasattr(self.memory, "_client") and self.memory._client:
247
241
  import json
248
242
 
249
- self.memory._redis.setex(request_key, int(timeout) + 60, json.dumps(request.to_dict()))
243
+ self.memory._client.setex(request_key, int(timeout) + 60, json.dumps(request.to_dict()))
250
244
  else:
251
- logger.warning("Cannot store approval request: unsupported memory type")
245
+ logger.warning("Cannot store approval request: no Redis backend available")
252
246
  except Exception as e:
253
247
  logger.error(f"Failed to store approval request: {e}")
254
248
  return ApprovalResponse(
@@ -294,12 +288,11 @@ class ApprovalGate:
294
288
  # Update request status to timeout
295
289
  request.status = "timeout"
296
290
  try:
297
- if hasattr(self.memory, "stash"):
298
- self.memory.stash(key=request_key, data=request.to_dict(), credentials=None, ttl_seconds=60)
299
- elif hasattr(self.memory, "_redis"):
291
+ # Use direct Redis access
292
+ if hasattr(self.memory, "_client") and self.memory._client:
300
293
  import json
301
294
 
302
- self.memory._redis.setex(request_key, 60, json.dumps(request.to_dict()))
295
+ self.memory._client.setex(request_key, 60, json.dumps(request.to_dict()))
303
296
  except Exception:
304
297
  pass
305
298
 
@@ -322,10 +315,10 @@ class ApprovalGate:
322
315
  if hasattr(self.memory, "retrieve"):
323
316
  data = self.memory.retrieve(response_key, credentials=None)
324
317
  # Try direct Redis access
325
- elif hasattr(self.memory, "_redis"):
318
+ elif hasattr(self.memory, "_client"):
326
319
  import json
327
320
 
328
- raw_data = self.memory._redis.get(response_key)
321
+ raw_data = self.memory._client.get(response_key)
329
322
  if raw_data:
330
323
  if isinstance(raw_data, bytes):
331
324
  raw_data = raw_data.decode("utf-8")
@@ -376,16 +369,13 @@ class ApprovalGate:
376
369
  # Store approval response (for workflow to retrieve)
377
370
  response_key = f"approval_response:{request_id}"
378
371
  try:
379
- if hasattr(self.memory, "stash"):
380
- self.memory.stash(
381
- key=response_key, data=response.to_dict(), credentials=None, ttl_seconds=300 # 5 min TTL
382
- )
383
- elif hasattr(self.memory, "_redis"):
372
+ # Use direct Redis access
373
+ if hasattr(self.memory, "_client") and self.memory._client:
384
374
  import json
385
375
 
386
- self.memory._redis.setex(response_key, 300, json.dumps(response.to_dict()))
376
+ self.memory._client.setex(response_key, 300, json.dumps(response.to_dict()))
387
377
  else:
388
- logger.warning("Cannot store approval response: unsupported memory type")
378
+ logger.warning("Cannot store approval response: no Redis backend available")
389
379
  return False
390
380
  except Exception as e:
391
381
  logger.error(f"Failed to store approval response: {e}")
@@ -396,10 +386,10 @@ class ApprovalGate:
396
386
  try:
397
387
  if hasattr(self.memory, "retrieve"):
398
388
  request_data = self.memory.retrieve(request_key, credentials=None)
399
- elif hasattr(self.memory, "_redis"):
389
+ elif hasattr(self.memory, "_client"):
400
390
  import json
401
391
 
402
- raw_data = self.memory._redis.get(request_key)
392
+ raw_data = self.memory._client.get(request_key)
403
393
  if raw_data:
404
394
  if isinstance(raw_data, bytes):
405
395
  raw_data = raw_data.decode("utf-8")
@@ -413,12 +403,11 @@ class ApprovalGate:
413
403
  request = ApprovalRequest.from_dict(request_data)
414
404
  request.status = "approved" if approved else "rejected"
415
405
 
416
- if hasattr(self.memory, "stash"):
417
- self.memory.stash(key=request_key, data=request.to_dict(), credentials=None, ttl_seconds=300)
418
- elif hasattr(self.memory, "_redis"):
406
+ # Use direct Redis access
407
+ if hasattr(self.memory, "_client") and self.memory._client:
419
408
  import json
420
409
 
421
- self.memory._redis.setex(request_key, 300, json.dumps(request.to_dict()))
410
+ self.memory._client.setex(request_key, 300, json.dumps(request.to_dict()))
422
411
  except Exception as e:
423
412
  logger.debug(f"Failed to update request status: {e}")
424
413
 
@@ -457,12 +446,12 @@ class ApprovalGate:
457
446
  >>> for request in pending:
458
447
  ... print(f"{request.approval_type}: {request.context}")
459
448
  """
460
- if not self.memory or not hasattr(self.memory, "_redis"):
449
+ if not self.memory or not hasattr(self.memory, "_client"):
461
450
  return []
462
451
 
463
452
  try:
464
453
  # Scan for approval_request:* keys
465
- keys = self.memory._redis.keys("approval_request:*")
454
+ keys = self.memory._client.keys("approval_request:*")
466
455
 
467
456
  requests = []
468
457
  for key in keys:
@@ -475,7 +464,7 @@ class ApprovalGate:
475
464
  else:
476
465
  import json
477
466
 
478
- raw_data = self.memory._redis.get(key)
467
+ raw_data = self.memory._client.get(key)
479
468
  if raw_data:
480
469
  if isinstance(raw_data, bytes):
481
470
  raw_data = raw_data.decode("utf-8")
@@ -512,11 +501,11 @@ class ApprovalGate:
512
501
  Returns:
513
502
  Number of requests cleared
514
503
  """
515
- if not self.memory or not hasattr(self.memory, "_redis"):
504
+ if not self.memory or not hasattr(self.memory, "_client"):
516
505
  return 0
517
506
 
518
507
  try:
519
- keys = self.memory._redis.keys("approval_request:*")
508
+ keys = self.memory._client.keys("approval_request:*")
520
509
  now = datetime.utcnow()
521
510
  cleared = 0
522
511
 
@@ -530,7 +519,7 @@ class ApprovalGate:
530
519
  else:
531
520
  import json
532
521
 
533
- raw_data = self.memory._redis.get(key)
522
+ raw_data = self.memory._client.get(key)
534
523
  if raw_data:
535
524
  if isinstance(raw_data, bytes):
536
525
  raw_data = raw_data.decode("utf-8")
@@ -548,12 +537,11 @@ class ApprovalGate:
548
537
  if elapsed > request.timeout_seconds and request.status == "pending":
549
538
  # Update to timeout status
550
539
  request.status = "timeout"
551
- if hasattr(self.memory, "stash"):
552
- self.memory.stash(key=key, data=request.to_dict(), credentials=None, ttl_seconds=60)
553
- elif hasattr(self.memory, "_redis"):
540
+ # Use direct Redis access
541
+ if hasattr(self.memory, "_client") and self.memory._client:
554
542
  import json
555
543
 
556
- self.memory._redis.setex(key, 60, json.dumps(request.to_dict()))
544
+ self.memory._client.setex(key, 60, json.dumps(request.to_dict()))
557
545
 
558
546
  cleared += 1
559
547
 
@@ -103,14 +103,14 @@ class EventStreamer:
103
103
  Publishes events to Redis Streams and provides methods for consuming
104
104
  events via polling or blocking reads.
105
105
 
106
- Stream naming: empathy:events:{event_type}
106
+ Stream naming: stream:{event_type}
107
107
  Examples:
108
- - empathy:events:agent_heartbeat
109
- - empathy:events:coordination_signal
110
- - empathy:events:workflow_progress
108
+ - stream:agent_heartbeat
109
+ - stream:coordination_signal
110
+ - stream:workflow_progress
111
111
  """
112
112
 
113
- STREAM_PREFIX = "empathy:events:"
113
+ STREAM_PREFIX = "stream:"
114
114
  MAX_STREAM_LENGTH = 10000 # Trim streams to last 10K events
115
115
  DEFAULT_BLOCK_MS = 5000 # 5 seconds blocking read timeout
116
116
 
@@ -142,7 +142,7 @@ class EventStreamer:
142
142
  event_type: Type of event
143
143
 
144
144
  Returns:
145
- Stream key (e.g., "empathy:events:agent_heartbeat")
145
+ Stream key (e.g., "stream:agent_heartbeat")
146
146
  """
147
147
  return f"{self.STREAM_PREFIX}{event_type}"
148
148
 
@@ -162,7 +162,7 @@ class EventStreamer:
162
162
  Returns:
163
163
  Event ID (Redis stream entry ID) if successful, empty string otherwise
164
164
  """
165
- if not self.memory or not hasattr(self.memory, "_redis"):
165
+ if not self.memory or not hasattr(self.memory, "_client") or not self.memory._client:
166
166
  logger.debug("Cannot publish event: no Redis backend")
167
167
  return ""
168
168
 
@@ -178,7 +178,7 @@ class EventStreamer:
178
178
 
179
179
  try:
180
180
  # Add to stream with automatic trimming (MAXLEN)
181
- event_id = self.memory._redis.xadd(
181
+ event_id = self.memory._client.xadd(
182
182
  stream_key,
183
183
  entry,
184
184
  maxlen=self.MAX_STREAM_LENGTH,
@@ -219,7 +219,7 @@ class EventStreamer:
219
219
  >>> for event in streamer.consume_events(event_types=["agent_heartbeat"]):
220
220
  ... print(f"Agent {event.data['agent_id']} status: {event.data['status']}")
221
221
  """
222
- if not self.memory or not hasattr(self.memory, "_redis"):
222
+ if not self.memory or not hasattr(self.memory, "_client") or not self.memory._client:
223
223
  logger.warning("Cannot consume events: no Redis backend")
224
224
  return
225
225
 
@@ -230,7 +230,7 @@ class EventStreamer:
230
230
  streams = {self._get_stream_key(et): start_id for et in event_types}
231
231
  else:
232
232
  # Subscribe to all event streams (expensive - requires KEYS scan)
233
- all_streams = self.memory._redis.keys(f"{self.STREAM_PREFIX}*")
233
+ all_streams = self.memory._client.keys(f"{self.STREAM_PREFIX}*")
234
234
  streams = {s.decode("utf-8") if isinstance(s, bytes) else s: start_id for s in all_streams}
235
235
 
236
236
  if not streams:
@@ -243,7 +243,7 @@ class EventStreamer:
243
243
  try:
244
244
  while True:
245
245
  # XREAD: blocking read from multiple streams
246
- results = self.memory._redis.xread(
246
+ results = self.memory._client.xread(
247
247
  last_ids,
248
248
  count=count,
249
249
  block=block_ms,
@@ -294,7 +294,7 @@ class EventStreamer:
294
294
  Returns:
295
295
  List of recent events (newest first)
296
296
  """
297
- if not self.memory or not hasattr(self.memory, "_redis"):
297
+ if not self.memory or not hasattr(self.memory, "_client") or not self.memory._client:
298
298
  logger.debug("Cannot get recent events: no Redis backend")
299
299
  return []
300
300
 
@@ -302,7 +302,7 @@ class EventStreamer:
302
302
 
303
303
  try:
304
304
  # XREVRANGE: get events in reverse chronological order
305
- results = self.memory._redis.xrevrange(
305
+ results = self.memory._client.xrevrange(
306
306
  stream_key,
307
307
  max=end_id,
308
308
  min=start_id,
@@ -333,13 +333,13 @@ class EventStreamer:
333
333
  Returns:
334
334
  Dictionary with stream info (length, first_entry, last_entry, etc.)
335
335
  """
336
- if not self.memory or not hasattr(self.memory, "_redis"):
336
+ if not self.memory or not hasattr(self.memory, "_client") or not self.memory._client:
337
337
  return {}
338
338
 
339
339
  stream_key = self._get_stream_key(event_type)
340
340
 
341
341
  try:
342
- info = self.memory._redis.xinfo_stream(stream_key)
342
+ info = self.memory._client.xinfo_stream(stream_key)
343
343
 
344
344
  # Decode bytes keys/values
345
345
  decoded_info = {}
@@ -365,13 +365,13 @@ class EventStreamer:
365
365
  Returns:
366
366
  True if deleted, False otherwise
367
367
  """
368
- if not self.memory or not hasattr(self.memory, "_redis"):
368
+ if not self.memory or not hasattr(self.memory, "_client") or not self.memory._client:
369
369
  return False
370
370
 
371
371
  stream_key = self._get_stream_key(event_type)
372
372
 
373
373
  try:
374
- result = self.memory._redis.delete(stream_key)
374
+ result = self.memory._client.delete(stream_key)
375
375
  return result > 0
376
376
  except Exception as e:
377
377
  logger.error(f"Failed to delete stream {event_type}: {e}")
@@ -387,14 +387,14 @@ class EventStreamer:
387
387
  Returns:
388
388
  Number of events trimmed
389
389
  """
390
- if not self.memory or not hasattr(self.memory, "_redis"):
390
+ if not self.memory or not hasattr(self.memory, "_client") or not self.memory._client:
391
391
  return 0
392
392
 
393
393
  stream_key = self._get_stream_key(event_type)
394
394
 
395
395
  try:
396
396
  # XTRIM: trim to approximate max length
397
- trimmed = self.memory._redis.xtrim(
397
+ trimmed = self.memory._client.xtrim(
398
398
  stream_key,
399
399
  maxlen=max_length,
400
400
  approximate=True,
@@ -229,16 +229,13 @@ class FeedbackLoop:
229
229
  key = f"feedback:{workflow_name}:{stage_name}:{tier}:{feedback_id}"
230
230
 
231
231
  try:
232
- if hasattr(self.memory, "stash"):
233
- self.memory.stash(
234
- key=key, data=entry.to_dict(), credentials=None, ttl_seconds=self.FEEDBACK_TTL
235
- )
236
- elif hasattr(self.memory, "_redis"):
232
+ # Use direct Redis access for custom TTL
233
+ if hasattr(self.memory, "_client") and self.memory._client:
237
234
  import json
238
235
 
239
- self.memory._redis.setex(key, self.FEEDBACK_TTL, json.dumps(entry.to_dict()))
236
+ self.memory._client.setex(key, self.FEEDBACK_TTL, json.dumps(entry.to_dict()))
240
237
  else:
241
- logger.warning("Cannot store feedback: unsupported memory type")
238
+ logger.warning("Cannot store feedback: no Redis backend available")
242
239
  return ""
243
240
  except Exception as e:
244
241
  logger.error(f"Failed to store feedback: {e}")
@@ -263,7 +260,7 @@ class FeedbackLoop:
263
260
  Returns:
264
261
  List of feedback entries (newest first)
265
262
  """
266
- if not self.memory or not hasattr(self.memory, "_redis"):
263
+ if not self.memory or not hasattr(self.memory, "_client"):
267
264
  return []
268
265
 
269
266
  # Convert tier to string if ModelTier enum
@@ -277,7 +274,7 @@ class FeedbackLoop:
277
274
  else:
278
275
  pattern = f"feedback:{workflow_name}:{stage_name}:*"
279
276
 
280
- keys = self.memory._redis.keys(pattern)
277
+ keys = self.memory._client.keys(pattern)
281
278
 
282
279
  entries = []
283
280
  for key in keys:
@@ -308,10 +305,10 @@ class FeedbackLoop:
308
305
  try:
309
306
  if hasattr(self.memory, "retrieve"):
310
307
  return self.memory.retrieve(key, credentials=None)
311
- elif hasattr(self.memory, "_redis"):
308
+ elif hasattr(self.memory, "_client"):
312
309
  import json
313
310
 
314
- data = self.memory._redis.get(key)
311
+ data = self.memory._client.get(key)
315
312
  if data:
316
313
  if isinstance(data, bytes):
317
314
  data = data.decode("utf-8")
@@ -494,13 +491,13 @@ class FeedbackLoop:
494
491
  Returns:
495
492
  List of (stage_name, stats) tuples for underperforming stages
496
493
  """
497
- if not self.memory or not hasattr(self.memory, "_redis"):
494
+ if not self.memory or not hasattr(self.memory, "_client"):
498
495
  return []
499
496
 
500
497
  try:
501
498
  # Find all feedback keys for this workflow
502
499
  pattern = f"feedback:{workflow_name}:*"
503
- keys = self.memory._redis.keys(pattern)
500
+ keys = self.memory._client.keys(pattern)
504
501
 
505
502
  # Extract unique stages
506
503
  stages = set()
@@ -537,7 +534,7 @@ class FeedbackLoop:
537
534
  Returns:
538
535
  Number of feedback entries cleared
539
536
  """
540
- if not self.memory or not hasattr(self.memory, "_redis"):
537
+ if not self.memory or not hasattr(self.memory, "_client"):
541
538
  return 0
542
539
 
543
540
  try:
@@ -546,11 +543,11 @@ class FeedbackLoop:
546
543
  else:
547
544
  pattern = f"feedback:{workflow_name}:*"
548
545
 
549
- keys = self.memory._redis.keys(pattern)
546
+ keys = self.memory._client.keys(pattern)
550
547
  if not keys:
551
548
  return 0
552
549
 
553
- deleted = self.memory._redis.delete(*keys)
550
+ deleted = self.memory._client.delete(*keys)
554
551
  return deleted
555
552
  except Exception as e:
556
553
  logger.error(f"Failed to clear feedback: {e}")
@@ -109,19 +109,22 @@ class BatchProcessingWorkflow:
109
109
  if not requests:
110
110
  raise ValueError("requests cannot be empty")
111
111
 
112
- # Convert to Anthropic batch format
112
+ # Convert to Anthropic Message Batches format
113
113
  api_requests = []
114
114
  for req in requests:
115
115
  model = get_model("anthropic", req.model_tier)
116
116
  if model is None:
117
117
  raise ValueError(f"Unknown model tier: {req.model_tier}")
118
118
 
119
+ # Use correct format with params wrapper
119
120
  api_requests.append(
120
121
  {
121
122
  "custom_id": req.task_id,
122
- "model": model.id,
123
- "messages": self._format_messages(req),
124
- "max_tokens": 4096,
123
+ "params": {
124
+ "model": model.id,
125
+ "messages": self._format_messages(req),
126
+ "max_tokens": 4096,
127
+ },
125
128
  }
126
129
  )
127
130
 
@@ -153,17 +156,58 @@ class BatchProcessingWorkflow:
153
156
  for req in requests
154
157
  ]
155
158
 
156
- # Parse results
159
+ # Parse results - new Message Batches API format
157
160
  results = []
158
161
  for raw in raw_results:
159
162
  task_id = raw.get("custom_id", "unknown")
163
+ result = raw.get("result", {})
164
+ result_type = result.get("type", "unknown")
165
+
166
+ if result_type == "succeeded":
167
+ # Extract message content from succeeded result
168
+ message = result.get("message", {})
169
+ content_blocks = message.get("content", [])
170
+
171
+ # Convert content blocks to simple output format
172
+ output_text = ""
173
+ for block in content_blocks:
174
+ if isinstance(block, dict) and block.get("type") == "text":
175
+ output_text += block.get("text", "")
176
+
177
+ output = {
178
+ "content": output_text,
179
+ "usage": message.get("usage", {}),
180
+ "model": message.get("model"),
181
+ "stop_reason": message.get("stop_reason"),
182
+ }
183
+ results.append(BatchResult(task_id=task_id, success=True, output=output))
184
+
185
+ elif result_type == "errored":
186
+ # Extract error from errored result
187
+ error = result.get("error", {})
188
+ error_msg = error.get("message", "Unknown error")
189
+ error_type = error.get("type", "unknown_error")
190
+ results.append(
191
+ BatchResult(task_id=task_id, success=False, error=f"{error_type}: {error_msg}")
192
+ )
193
+
194
+ elif result_type == "expired":
195
+ results.append(
196
+ BatchResult(task_id=task_id, success=False, error="Request expired")
197
+ )
198
+
199
+ elif result_type == "canceled":
200
+ results.append(
201
+ BatchResult(task_id=task_id, success=False, error="Request canceled")
202
+ )
160
203
 
161
- if "error" in raw:
162
- error_msg = raw["error"].get("message", "Unknown error")
163
- results.append(BatchResult(task_id=task_id, success=False, error=error_msg))
164
204
  else:
165
205
  results.append(
166
- BatchResult(task_id=task_id, success=True, output=raw.get("response"))
206
+ BatchResult(
207
+ task_id=task_id,
208
+ success=False,
209
+ error=f"Unknown result type: {result_type}",
210
+ )
167
211
  )
168
212
 
169
213
  # Log summary
@@ -201,7 +245,9 @@ class BatchProcessingWorkflow:
201
245
  logger.warning(
202
246
  f"Missing required field {e} for task {request.task_type}, using raw input"
203
247
  )
204
- content = prompt_template.format(input=json.dumps(request.input_data))
248
+ # Use default template instead of the specific one
249
+ default_template = "Process the following:\n\n{input}"
250
+ content = default_template.format(input=json.dumps(request.input_data))
205
251
 
206
252
  return [{"role": "user", "content": content}]
207
253
 
@@ -1,173 +0,0 @@
1
- """VS Code Extension Bridge
2
-
3
- Provides functions to write data that the VS Code extension can pick up.
4
- Enables Claude Code CLI output to appear in VS Code webview panels.
5
-
6
- Copyright 2026 Smart-AI-Memory
7
- Licensed under Fair Source License 0.9
8
- """
9
-
10
- import json
11
- from dataclasses import asdict, dataclass
12
- from datetime import datetime
13
- from pathlib import Path
14
- from typing import Any
15
-
16
-
17
- @dataclass
18
- class ReviewFinding:
19
- """A code review finding."""
20
-
21
- id: str
22
- file: str
23
- line: int
24
- severity: str # 'critical' | 'high' | 'medium' | 'low' | 'info'
25
- category: str # 'security' | 'performance' | 'maintainability' | 'style' | 'correctness'
26
- message: str
27
- column: int = 1
28
- details: str | None = None
29
- recommendation: str | None = None
30
-
31
-
32
- @dataclass
33
- class CodeReviewResult:
34
- """Code review results for VS Code bridge."""
35
-
36
- findings: list[dict[str, Any]]
37
- summary: dict[str, Any]
38
- verdict: str # 'approve' | 'approve_with_suggestions' | 'request_changes' | 'reject'
39
- security_score: int
40
- formatted_report: str
41
- model_tier_used: str
42
- timestamp: str
43
-
44
-
45
- def get_empathy_dir() -> Path:
46
- """Get the .empathy directory, creating if needed."""
47
- empathy_dir = Path(".empathy")
48
- empathy_dir.mkdir(exist_ok=True)
49
- return empathy_dir
50
-
51
-
52
- def write_code_review_results(
53
- findings: list[dict[str, Any]] | None = None,
54
- summary: dict[str, Any] | None = None,
55
- verdict: str = "approve_with_suggestions",
56
- security_score: int = 85,
57
- formatted_report: str = "",
58
- model_tier_used: str = "capable",
59
- ) -> Path:
60
- """Write code review results for VS Code extension to pick up.
61
-
62
- Args:
63
- findings: List of finding dicts with keys: id, file, line, severity, category, message
64
- summary: Summary dict with keys: total_findings, by_severity, by_category, files_affected
65
- verdict: One of 'approve', 'approve_with_suggestions', 'request_changes', 'reject'
66
- security_score: 0-100 score
67
- formatted_report: Markdown formatted report
68
- model_tier_used: 'cheap', 'capable', or 'premium'
69
-
70
- Returns:
71
- Path to the written file
72
- """
73
- findings = findings or []
74
-
75
- # Build summary if not provided
76
- if summary is None:
77
- by_severity: dict[str, int] = {}
78
- by_category: dict[str, int] = {}
79
- files_affected: set[str] = set()
80
-
81
- for f in findings:
82
- sev = f.get("severity", "info")
83
- cat = f.get("category", "correctness")
84
- by_severity[sev] = by_severity.get(sev, 0) + 1
85
- by_category[cat] = by_category.get(cat, 0) + 1
86
- if f.get("file"):
87
- files_affected.add(f["file"])
88
-
89
- summary = {
90
- "total_findings": len(findings),
91
- "by_severity": by_severity,
92
- "by_category": by_category,
93
- "files_affected": list(files_affected),
94
- }
95
-
96
- result = CodeReviewResult(
97
- findings=findings,
98
- summary=summary,
99
- verdict=verdict,
100
- security_score=security_score,
101
- formatted_report=formatted_report,
102
- model_tier_used=model_tier_used,
103
- timestamp=datetime.now().isoformat(),
104
- )
105
-
106
- output_path = get_empathy_dir() / "code-review-results.json"
107
-
108
- with open(output_path, "w") as f:
109
- json.dump(asdict(result), f, indent=2)
110
-
111
- return output_path
112
-
113
-
114
- def write_pr_review_results(
115
- pr_number: int | str,
116
- title: str,
117
- findings: list[dict[str, Any]],
118
- verdict: str = "approve_with_suggestions",
119
- summary_text: str = "",
120
- ) -> Path:
121
- """Write PR review results for VS Code extension.
122
-
123
- Convenience wrapper for PR reviews from GitHub.
124
-
125
- Args:
126
- pr_number: The PR number
127
- title: PR title
128
- findings: List of review findings
129
- verdict: Review verdict
130
- summary_text: Summary of the review
131
-
132
- Returns:
133
- Path to the written file
134
- """
135
- formatted_report = f"""## PR #{pr_number}: {title}
136
-
137
- {summary_text}
138
-
139
- ### Findings ({len(findings)})
140
-
141
- """
142
- for f in findings:
143
- formatted_report += f"- **{f.get('severity', 'info').upper()}** [{f.get('file', 'unknown')}:{f.get('line', 0)}]: {f.get('message', '')}\n"
144
-
145
- return write_code_review_results(
146
- findings=findings,
147
- verdict=verdict,
148
- formatted_report=formatted_report,
149
- model_tier_used="capable",
150
- )
151
-
152
-
153
- # Quick helper for Claude Code to call
154
- def send_to_vscode(
155
- message: str,
156
- findings: list[dict[str, Any]] | None = None,
157
- verdict: str = "approve_with_suggestions",
158
- ) -> str:
159
- """Quick helper to send review results to VS Code.
160
-
161
- Usage in Claude Code:
162
- from empathy_os.vscode_bridge import send_to_vscode
163
- send_to_vscode("Review complete", findings=[...])
164
-
165
- Returns:
166
- Confirmation message
167
- """
168
- path = write_code_review_results(
169
- findings=findings or [],
170
- formatted_report=message,
171
- verdict=verdict,
172
- )
173
- return f"Results written to {path} - VS Code will update automatically"