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.
- {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/METADATA +53 -9
- {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/RECORD +28 -31
- empathy_llm_toolkit/providers.py +175 -35
- empathy_llm_toolkit/utils/tokens.py +150 -30
- empathy_os/__init__.py +1 -1
- empathy_os/cli/commands/batch.py +256 -0
- empathy_os/cli/commands/cache.py +248 -0
- empathy_os/cli/commands/inspect.py +1 -2
- empathy_os/cli/commands/metrics.py +1 -1
- empathy_os/cli/commands/routing.py +285 -0
- empathy_os/cli/commands/workflow.py +2 -2
- empathy_os/cli/parsers/__init__.py +6 -0
- empathy_os/cli/parsers/batch.py +118 -0
- empathy_os/cli/parsers/cache.py +65 -0
- empathy_os/cli/parsers/routing.py +110 -0
- empathy_os/dashboard/standalone_server.py +22 -11
- empathy_os/metrics/collector.py +31 -0
- empathy_os/models/token_estimator.py +21 -13
- empathy_os/telemetry/agent_coordination.py +12 -14
- empathy_os/telemetry/agent_tracking.py +18 -19
- empathy_os/telemetry/approval_gates.py +27 -39
- empathy_os/telemetry/event_streaming.py +19 -19
- empathy_os/telemetry/feedback_loop.py +13 -16
- empathy_os/workflows/batch_processing.py +56 -10
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/progressive/README 2.md +0 -454
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/WHEEL +0 -0
- {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.0.0.dist-info → empathy_framework-5.0.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
240
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
298
|
-
|
|
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.
|
|
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, "
|
|
318
|
+
elif hasattr(self.memory, "_client"):
|
|
326
319
|
import json
|
|
327
320
|
|
|
328
|
-
raw_data = self.memory.
|
|
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
|
-
|
|
380
|
-
|
|
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.
|
|
376
|
+
self.memory._client.setex(response_key, 300, json.dumps(response.to_dict()))
|
|
387
377
|
else:
|
|
388
|
-
logger.warning("Cannot store approval response:
|
|
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, "
|
|
389
|
+
elif hasattr(self.memory, "_client"):
|
|
400
390
|
import json
|
|
401
391
|
|
|
402
|
-
raw_data = self.memory.
|
|
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
|
-
|
|
417
|
-
|
|
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.
|
|
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, "
|
|
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.
|
|
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.
|
|
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, "
|
|
504
|
+
if not self.memory or not hasattr(self.memory, "_client"):
|
|
516
505
|
return 0
|
|
517
506
|
|
|
518
507
|
try:
|
|
519
|
-
keys = self.memory.
|
|
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.
|
|
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
|
-
|
|
552
|
-
|
|
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.
|
|
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:
|
|
106
|
+
Stream naming: stream:{event_type}
|
|
107
107
|
Examples:
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
108
|
+
- stream:agent_heartbeat
|
|
109
|
+
- stream:coordination_signal
|
|
110
|
+
- stream:workflow_progress
|
|
111
111
|
"""
|
|
112
112
|
|
|
113
|
-
STREAM_PREFIX = "
|
|
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., "
|
|
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, "
|
|
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.
|
|
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, "
|
|
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.
|
|
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.
|
|
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, "
|
|
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.
|
|
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, "
|
|
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.
|
|
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, "
|
|
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.
|
|
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, "
|
|
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.
|
|
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
|
-
|
|
233
|
-
|
|
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.
|
|
236
|
+
self.memory._client.setex(key, self.FEEDBACK_TTL, json.dumps(entry.to_dict()))
|
|
240
237
|
else:
|
|
241
|
-
logger.warning("Cannot store feedback:
|
|
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, "
|
|
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.
|
|
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, "
|
|
308
|
+
elif hasattr(self.memory, "_client"):
|
|
312
309
|
import json
|
|
313
310
|
|
|
314
|
-
data = self.memory.
|
|
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, "
|
|
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.
|
|
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, "
|
|
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.
|
|
546
|
+
keys = self.memory._client.keys(pattern)
|
|
550
547
|
if not keys:
|
|
551
548
|
return 0
|
|
552
549
|
|
|
553
|
-
deleted = self.memory.
|
|
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
|
|
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
|
-
"
|
|
123
|
-
|
|
124
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
empathy_os/vscode_bridge 2.py
DELETED
|
@@ -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"
|