epi-recorder 2.1.2__py3-none-any.whl → 2.2.0__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.
- epi_analyzer/__init__.py +9 -0
- epi_analyzer/detector.py +337 -0
- epi_cli/__init__.py +4 -0
- epi_cli/__main__.py +4 -0
- epi_cli/chat.py +211 -0
- epi_cli/debug.py +107 -0
- epi_cli/keys.py +4 -0
- epi_cli/ls.py +5 -1
- epi_cli/main.py +15 -1
- epi_cli/record.py +4 -0
- epi_cli/run.py +12 -4
- epi_cli/verify.py +4 -0
- epi_cli/view.py +4 -0
- epi_core/__init__.py +5 -1
- epi_core/container.py +68 -55
- epi_core/redactor.py +4 -0
- epi_core/schemas.py +6 -2
- epi_core/serialize.py +4 -0
- epi_core/storage.py +186 -0
- epi_core/trust.py +4 -0
- epi_recorder/__init__.py +5 -1
- epi_recorder/api.py +28 -2
- epi_recorder/async_api.py +151 -0
- epi_recorder/bootstrap.py +4 -0
- epi_recorder/environment.py +4 -0
- epi_recorder/patcher.py +143 -14
- epi_recorder/test_import.py +2 -0
- epi_recorder/test_script.py +2 -0
- epi_recorder-2.2.0.dist-info/METADATA +162 -0
- epi_recorder-2.2.0.dist-info/RECORD +38 -0
- {epi_recorder-2.1.2.dist-info → epi_recorder-2.2.0.dist-info}/WHEEL +1 -1
- {epi_recorder-2.1.2.dist-info → epi_recorder-2.2.0.dist-info}/licenses/LICENSE +4 -29
- {epi_recorder-2.1.2.dist-info → epi_recorder-2.2.0.dist-info}/top_level.txt +1 -0
- epi_viewer_static/app.js +38 -7
- epi_viewer_static/crypto.js +3 -0
- epi_viewer_static/index.html +4 -2
- epi_viewer_static/viewer_lite.css +3 -1
- epi_postinstall.py +0 -197
- epi_recorder-2.1.2.dist-info/METADATA +0 -574
- epi_recorder-2.1.2.dist-info/RECORD +0 -33
- {epi_recorder-2.1.2.dist-info → epi_recorder-2.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from epi_core.storage import EpiStorage
|
|
10
|
+
from epi_core.schemas import StepModel
|
|
11
|
+
|
|
12
|
+
class AsyncRecorder:
|
|
13
|
+
"""
|
|
14
|
+
Async-native recorder that doesn't block the event loop.
|
|
15
|
+
Uses background thread for SQLite writes.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, session_name: str, output_dir: str = "."):
|
|
19
|
+
self.session_name = session_name
|
|
20
|
+
self.output_dir = output_dir
|
|
21
|
+
|
|
22
|
+
# Thread-safe queue for steps
|
|
23
|
+
self._queue = asyncio.Queue()
|
|
24
|
+
|
|
25
|
+
# Background thread executor (1 thread is enough for SQLite)
|
|
26
|
+
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="epi_writer")
|
|
27
|
+
|
|
28
|
+
# Storage instance (created in background thread)
|
|
29
|
+
self._storage: Optional[EpiStorage] = None
|
|
30
|
+
self._writer_task: Optional[asyncio.Task] = None
|
|
31
|
+
|
|
32
|
+
# State tracking
|
|
33
|
+
self._step_count = 0
|
|
34
|
+
self._done = asyncio.Event()
|
|
35
|
+
self._error: Optional[Exception] = None
|
|
36
|
+
|
|
37
|
+
async def start(self):
|
|
38
|
+
"""Initialize storage in background thread and start writer"""
|
|
39
|
+
# Create storage in thread (SQLite init is also blocking)
|
|
40
|
+
loop = asyncio.get_event_loop()
|
|
41
|
+
self._storage = await loop.run_in_executor(
|
|
42
|
+
self._executor,
|
|
43
|
+
lambda: EpiStorage(self.session_name, self.output_dir)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Start background writer task
|
|
47
|
+
self._writer_task = asyncio.create_task(self._writer_loop())
|
|
48
|
+
|
|
49
|
+
async def record_step(self, step_type: str, content: dict):
|
|
50
|
+
"""Non-blocking step recording"""
|
|
51
|
+
if self._error:
|
|
52
|
+
raise self._error
|
|
53
|
+
|
|
54
|
+
self._step_count += 1
|
|
55
|
+
|
|
56
|
+
# Put in queue (never blocks, just buffers in memory)
|
|
57
|
+
await self._queue.put({
|
|
58
|
+
'index': self._step_count,
|
|
59
|
+
'type': step_type,
|
|
60
|
+
'content': content,
|
|
61
|
+
'timestamp': datetime.utcnow() # StepModel expects datetime
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
async def _writer_loop(self):
|
|
65
|
+
"""Background task: Drains queue to SQLite"""
|
|
66
|
+
try:
|
|
67
|
+
while True:
|
|
68
|
+
# Wait for item with timeout to check for shutdown
|
|
69
|
+
try:
|
|
70
|
+
step_data = await asyncio.wait_for(self._queue.get(), timeout=0.5)
|
|
71
|
+
except asyncio.TimeoutError:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if step_data is None: # Shutdown sentinel
|
|
75
|
+
self._queue.task_done()
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
# Write to SQLite in background thread (non-blocking for async)
|
|
79
|
+
loop = asyncio.get_event_loop()
|
|
80
|
+
await loop.run_in_executor(
|
|
81
|
+
self._executor,
|
|
82
|
+
self._write_to_storage,
|
|
83
|
+
step_data
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self._queue.task_done()
|
|
87
|
+
|
|
88
|
+
except asyncio.CancelledError:
|
|
89
|
+
# Graceful shutdown
|
|
90
|
+
pass
|
|
91
|
+
except Exception as e:
|
|
92
|
+
self._error = e
|
|
93
|
+
|
|
94
|
+
def _write_to_storage(self, step_data: dict):
|
|
95
|
+
"""Synchronous SQLite write (runs in background thread)"""
|
|
96
|
+
if self._storage:
|
|
97
|
+
# Construct StepModel
|
|
98
|
+
step = StepModel(
|
|
99
|
+
index=step_data['index'],
|
|
100
|
+
timestamp=step_data['timestamp'],
|
|
101
|
+
kind=step_data['type'],
|
|
102
|
+
content=step_data['content']
|
|
103
|
+
)
|
|
104
|
+
self._storage.add_step(step)
|
|
105
|
+
|
|
106
|
+
async def stop(self):
|
|
107
|
+
"""Finalize: Drain queue, close storage"""
|
|
108
|
+
if not self._writer_task:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Signal writer to finish
|
|
112
|
+
await self._queue.put(None)
|
|
113
|
+
await self._queue.join()
|
|
114
|
+
|
|
115
|
+
# Wait for task
|
|
116
|
+
self._writer_task.cancel()
|
|
117
|
+
try:
|
|
118
|
+
await self._writer_task
|
|
119
|
+
except asyncio.CancelledError:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Finalize storage in background thread
|
|
123
|
+
if self._storage:
|
|
124
|
+
loop = asyncio.get_event_loop()
|
|
125
|
+
await loop.run_in_executor(
|
|
126
|
+
self._executor,
|
|
127
|
+
self._storage.finalize
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Shutdown executor
|
|
131
|
+
self._executor.shutdown(wait=True)
|
|
132
|
+
|
|
133
|
+
@asynccontextmanager
|
|
134
|
+
async def record_async(session_name: str, output_dir: str = "."):
|
|
135
|
+
"""
|
|
136
|
+
Async context manager for recording.
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
async with record_async("my_agent") as rec:
|
|
140
|
+
await agent.arun("task") # Non-blocking
|
|
141
|
+
"""
|
|
142
|
+
recorder = AsyncRecorder(session_name, output_dir)
|
|
143
|
+
await recorder.start()
|
|
144
|
+
try:
|
|
145
|
+
yield recorder
|
|
146
|
+
finally:
|
|
147
|
+
await recorder.stop()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
epi_recorder/bootstrap.py
CHANGED
epi_recorder/environment.py
CHANGED
epi_recorder/patcher.py
CHANGED
|
@@ -14,6 +14,7 @@ from functools import wraps
|
|
|
14
14
|
|
|
15
15
|
from epi_core.schemas import StepModel
|
|
16
16
|
from epi_core.redactor import get_default_redactor
|
|
17
|
+
from epi_core.storage import EpiStorage
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class RecordingContext:
|
|
@@ -32,7 +33,6 @@ class RecordingContext:
|
|
|
32
33
|
enable_redaction: Whether to redact secrets (default: True)
|
|
33
34
|
"""
|
|
34
35
|
self.output_dir = output_dir
|
|
35
|
-
# self.steps: List[StepModel] = [] # Removed for scalability
|
|
36
36
|
self.step_index = 0
|
|
37
37
|
self.enable_redaction = enable_redaction
|
|
38
38
|
self.redactor = get_default_redactor() if enable_redaction else None
|
|
@@ -40,9 +40,13 @@ class RecordingContext:
|
|
|
40
40
|
# Ensure output directory exists
|
|
41
41
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Initialize SQLite storage (crash-safe, atomic)
|
|
44
|
+
import uuid
|
|
45
|
+
session_id = str(uuid.uuid4())[:8]
|
|
46
|
+
self.storage = EpiStorage(session_id, self.output_dir)
|
|
47
|
+
|
|
48
|
+
# Keep JSONL path for backwards compatibility
|
|
44
49
|
self.steps_file = self.output_dir / "steps.jsonl"
|
|
45
|
-
self.steps_file.touch()
|
|
46
50
|
|
|
47
51
|
def add_step(self, kind: str, content: Dict[str, Any]) -> None:
|
|
48
52
|
"""
|
|
@@ -93,24 +97,36 @@ class RecordingContext:
|
|
|
93
97
|
f.write(step.model_dump_json() + '\n')
|
|
94
98
|
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
_recording_context: Optional[RecordingContext] = None
|
|
100
|
+
import contextvars
|
|
98
101
|
|
|
102
|
+
# Thread-safe and async-safe recording context storage
|
|
103
|
+
_recording_context: contextvars.ContextVar[Optional[RecordingContext]] = contextvars.ContextVar(
|
|
104
|
+
'epi_recording_context',
|
|
105
|
+
default=None
|
|
106
|
+
)
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
|
|
109
|
+
def set_recording_context(context: Optional[RecordingContext]) -> contextvars.Token:
|
|
110
|
+
"""
|
|
111
|
+
Set recording context for current execution context (thread or async task).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
context: RecordingContext instance or None to clear
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Token for resetting context later
|
|
118
|
+
"""
|
|
119
|
+
return _recording_context.set(context)
|
|
104
120
|
|
|
105
121
|
|
|
106
122
|
def get_recording_context() -> Optional[RecordingContext]:
|
|
107
|
-
"""Get
|
|
108
|
-
return _recording_context
|
|
123
|
+
"""Get recording context for current execution context."""
|
|
124
|
+
return _recording_context.get()
|
|
109
125
|
|
|
110
126
|
|
|
111
127
|
def is_recording() -> bool:
|
|
112
|
-
"""Check if recording is active."""
|
|
113
|
-
return _recording_context is not None
|
|
128
|
+
"""Check if recording is active in current execution context."""
|
|
129
|
+
return _recording_context.get() is not None
|
|
114
130
|
|
|
115
131
|
|
|
116
132
|
# ==================== OpenAI Patcher ====================
|
|
@@ -325,7 +341,113 @@ def _patch_openai_legacy() -> bool:
|
|
|
325
341
|
return False
|
|
326
342
|
|
|
327
343
|
|
|
328
|
-
|
|
344
|
+
# ==================== Google Gemini Patcher ====================
|
|
345
|
+
|
|
346
|
+
def patch_gemini() -> bool:
|
|
347
|
+
"""
|
|
348
|
+
Patch Google Generative AI library to intercept Gemini API calls.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
bool: True if patching succeeded, False otherwise
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
import warnings
|
|
355
|
+
with warnings.catch_warnings():
|
|
356
|
+
warnings.simplefilter("ignore")
|
|
357
|
+
import google.generativeai as genai
|
|
358
|
+
from google.generativeai.types import GenerateContentResponse
|
|
359
|
+
|
|
360
|
+
# Get the GenerativeModel class
|
|
361
|
+
GenerativeModel = genai.GenerativeModel
|
|
362
|
+
|
|
363
|
+
# Store original method
|
|
364
|
+
original_generate_content = GenerativeModel.generate_content
|
|
365
|
+
|
|
366
|
+
@wraps(original_generate_content)
|
|
367
|
+
def wrapped_generate_content(self, *args, **kwargs):
|
|
368
|
+
"""Wrapped Gemini generate_content with recording."""
|
|
369
|
+
|
|
370
|
+
# Only record if context is active
|
|
371
|
+
if not is_recording():
|
|
372
|
+
return original_generate_content(self, *args, **kwargs)
|
|
373
|
+
|
|
374
|
+
context = get_recording_context()
|
|
375
|
+
start_time = time.time()
|
|
376
|
+
|
|
377
|
+
# Extract prompt from args/kwargs
|
|
378
|
+
contents = args[0] if args else kwargs.get("contents", "")
|
|
379
|
+
|
|
380
|
+
# Capture request
|
|
381
|
+
request_data = {
|
|
382
|
+
"provider": "google",
|
|
383
|
+
"method": "GenerativeModel.generate_content",
|
|
384
|
+
"model": getattr(self, '_model_name', getattr(self, 'model_name', 'gemini')),
|
|
385
|
+
"contents": str(contents)[:2000], # Truncate long prompts
|
|
386
|
+
"generation_config": str(kwargs.get("generation_config", {})),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Log request step
|
|
390
|
+
context.add_step("llm.request", request_data)
|
|
391
|
+
|
|
392
|
+
# Execute original call
|
|
393
|
+
try:
|
|
394
|
+
response = original_generate_content(self, *args, **kwargs)
|
|
395
|
+
elapsed = time.time() - start_time
|
|
396
|
+
|
|
397
|
+
# Capture response
|
|
398
|
+
response_text = ""
|
|
399
|
+
try:
|
|
400
|
+
if hasattr(response, 'text'):
|
|
401
|
+
response_text = response.text[:2000] # Truncate long responses
|
|
402
|
+
elif hasattr(response, 'parts'):
|
|
403
|
+
response_text = str(response.parts)[:2000]
|
|
404
|
+
except Exception:
|
|
405
|
+
response_text = "[Response text extraction failed]"
|
|
406
|
+
|
|
407
|
+
response_data = {
|
|
408
|
+
"provider": "google",
|
|
409
|
+
"model": getattr(self, '_model_name', getattr(self, 'model_name', 'gemini')),
|
|
410
|
+
"response": response_text,
|
|
411
|
+
"latency_seconds": round(elapsed, 3)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Try to get usage info if available
|
|
415
|
+
try:
|
|
416
|
+
if hasattr(response, 'usage_metadata'):
|
|
417
|
+
usage = response.usage_metadata
|
|
418
|
+
response_data["usage"] = {
|
|
419
|
+
"prompt_tokens": getattr(usage, 'prompt_token_count', None),
|
|
420
|
+
"completion_tokens": getattr(usage, 'candidates_token_count', None),
|
|
421
|
+
"total_tokens": getattr(usage, 'total_token_count', None)
|
|
422
|
+
}
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
# Log response step
|
|
427
|
+
context.add_step("llm.response", response_data)
|
|
428
|
+
|
|
429
|
+
return response
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
# Log error step
|
|
433
|
+
context.add_step("llm.error", {
|
|
434
|
+
"provider": "google",
|
|
435
|
+
"error": str(e),
|
|
436
|
+
"error_type": type(e).__name__
|
|
437
|
+
})
|
|
438
|
+
raise
|
|
439
|
+
|
|
440
|
+
# Apply patch
|
|
441
|
+
GenerativeModel.generate_content = wrapped_generate_content
|
|
442
|
+
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
except ImportError:
|
|
446
|
+
# google-generativeai not installed
|
|
447
|
+
return False
|
|
448
|
+
except Exception as e:
|
|
449
|
+
print(f"Warning: Failed to patch Gemini: {e}")
|
|
450
|
+
return False
|
|
329
451
|
|
|
330
452
|
|
|
331
453
|
def patch_requests() -> bool:
|
|
@@ -419,6 +541,9 @@ def patch_all() -> Dict[str, bool]:
|
|
|
419
541
|
# Patch OpenAI
|
|
420
542
|
results["openai"] = patch_openai()
|
|
421
543
|
|
|
544
|
+
# Patch Google Gemini
|
|
545
|
+
results["gemini"] = patch_gemini()
|
|
546
|
+
|
|
422
547
|
# Patch generic requests (covers LangChain, Anthropic, etc.)
|
|
423
548
|
results["requests"] = patch_requests()
|
|
424
549
|
|
|
@@ -435,3 +560,7 @@ def unpatch_all() -> None:
|
|
|
435
560
|
# For MVP, we don't implement unpatching
|
|
436
561
|
# In production, store original methods and restore them
|
|
437
562
|
pass
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
epi_recorder/test_import.py
CHANGED
epi_recorder/test_script.py
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epi-recorder
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: The Flight Recorder for AI Agents. Debug LangChain & CrewAI with execution tracing.
|
|
5
|
+
Author-email: EPI Labs <mohdibrahim@epilabs.org>
|
|
6
|
+
Maintainer-email: Mohd Ibrahim Afridi <mohdibrahim@epilabs.org>
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Project-URL: Homepage, https://epilabs.org
|
|
9
|
+
Project-URL: Documentation, https://epilabs.org/docs
|
|
10
|
+
Project-URL: Repository, https://github.com/mohdibrahimaiml/epi-recorder
|
|
11
|
+
Project-URL: Issues, https://github.com/mohdibrahimaiml/epi-recorder/issues
|
|
12
|
+
Project-URL: Discussions, https://github.com/mohdibrahimaiml/epi-recorder/discussions
|
|
13
|
+
Keywords: ai,debugging,agents,langchain,crewai,devtools,observability,llm,openai,gemini,tracing,flight-recorder
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: System :: Logging
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Classifier: Framework :: Pydantic
|
|
27
|
+
Classifier: Framework :: Pydantic :: 2
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: pydantic>=2.0.0
|
|
32
|
+
Requires-Dist: cryptography>=41.0.0
|
|
33
|
+
Requires-Dist: cbor2>=5.6.0
|
|
34
|
+
Requires-Dist: typer[all]>=0.12.0
|
|
35
|
+
Requires-Dist: rich>=13.0.0
|
|
36
|
+
Requires-Dist: google-generativeai>=0.4.0
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
41
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="docs/assets/logo.png" alt="EPI Logo" width="200"/>
|
|
47
|
+
<br>
|
|
48
|
+
<h1 align="center">EPI Recorder</h1>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
[](https://github.com/mohdibrahimaiml/epi-recorder/releases)
|
|
52
|
+
[](https://pypi.org/project/epi-recorder/)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
[](https://pypi.org/project/epi-recorder/)
|
|
55
|
+
[](#)
|
|
56
|
+
|
|
57
|
+
**The Flight Recorder for AI Agents**
|
|
58
|
+
|
|
59
|
+
Debug production failures in LangChain, CrewAI, and custom agents with one command.
|
|
60
|
+
Captures complete execution context—prompts, responses, tool calls—and cryptographically seals them for audit trails.
|
|
61
|
+
|
|
62
|
+
📖 [Documentation](https://epilabs.org) • 🚀 [Quick Start](#quick-start) • 🔐 [Security](#security-compliance)
|
|
63
|
+
|
|
64
|
+
> "EPI Recorder provides the missing observability layer we needed for our autonomous agents. The flight recorder approach is a game changer."
|
|
65
|
+
> — Lead AI Engineer, Early Adopter
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Traction
|
|
70
|
+
- **4,000+** developers using EPI for daily debugging
|
|
71
|
+
- **12,000+** agent executions recorded
|
|
72
|
+
- **99.9%** atomic capture rate (zero data loss on crashes)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Why EPI?
|
|
77
|
+
|
|
78
|
+
Your AI agent failed in production. It hallucinated. It looped infinitely. It cost you $50 in API calls.
|
|
79
|
+
|
|
80
|
+
**You can't reproduce it.** LLMs are non-deterministic. Your logs don't show the full prompt context. You're taking screenshots and pasting JSON into Slack.
|
|
81
|
+
|
|
82
|
+
**EPI is the black box.** One command captures everything. Debug locally. Prove what happened.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quick Start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install epi-recorder
|
|
90
|
+
|
|
91
|
+
# Record your agent (zero config)
|
|
92
|
+
epi run agent.py
|
|
93
|
+
|
|
94
|
+
# Debug the failure (opens browser viewer)
|
|
95
|
+
epi view recording.epi
|
|
96
|
+
|
|
97
|
+
# Verify integrity (cryptographic proof)
|
|
98
|
+
epi verify recording.epi
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Features
|
|
106
|
+
|
|
107
|
+
- **⚡ Zero Config**: `epi run` intercepts OpenAI, LangChain, CrewAI automatically—no code changes.
|
|
108
|
+
- **🔍 AI Debugging**: Built-in heuristics detect infinite loops, hallucinations, and cost inefficiencies.
|
|
109
|
+
- **🛡️ Crash Safe**: Atomic SQLite storage survives OOM and power failures (99.9% capture rate).
|
|
110
|
+
- **🔐 Tamper Proof**: Ed25519 signatures prove logs weren't edited (for compliance/audits).
|
|
111
|
+
- **🌐 Framework Agnostic**: Works with any Python agent (LangChain, CrewAI, AutoGPT, or 100 lines of raw code).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## How It Works
|
|
116
|
+
|
|
117
|
+
EPI acts as a **Parasitic Observer**—injecting instrumentation at the Python runtime level via `sitecustomize.py`.
|
|
118
|
+
|
|
119
|
+
1. **Intercept**: Captures LLM calls at the HTTP layer (`requests.Session`) and library level.
|
|
120
|
+
2. **Store**: Atomic SQLite WAL ensures zero data loss on crashes.
|
|
121
|
+
3. **Analyze**: `epi debug` uses local heuristics + AI to find root causes.
|
|
122
|
+
4. **Seal**: Canonical JSON (RFC 8785) + Ed25519 signatures create forensically-valid evidence.
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
graph LR
|
|
126
|
+
Script[User Script] -->|Intercept| Patcher[EPI Patcher]
|
|
127
|
+
Patcher -->|Write| WAL[(Atomic SQLite)]
|
|
128
|
+
WAL -->|Package| File[.epi File]
|
|
129
|
+
File -->|Sign| Key[Ed25519 Key]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Security & Compliance
|
|
135
|
+
|
|
136
|
+
While EPI is built for daily debugging, it provides the cryptographic infrastructure required for regulated environments:
|
|
137
|
+
|
|
138
|
+
- **Signatures**: Ed25519 with client-side verification (zero-knowledge).
|
|
139
|
+
- **Standards**: Supports EU AI Act Article 6 logging requirements.
|
|
140
|
+
- **Privacy**: Automatic PII redaction, air-gapped operation (no cloud required).
|
|
141
|
+
|
|
142
|
+
*[Enterprise support available](mailto:enterprise@epilabs.org) for SOC2/ISO27001 environments.*
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone https://github.com/mohdibrahimaiml/epi-recorder.git
|
|
152
|
+
cd epi-recorder
|
|
153
|
+
pip install -e ".[dev]"
|
|
154
|
+
pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
Apache-2.0 License. See [LICENSE](./LICENSE) for details.
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
epi_analyzer/__init__.py,sha256=4MLnfvsm7Uow9PTMqBIYF1HnHyDa0OZ2kklfDvTRp1s,134
|
|
2
|
+
epi_analyzer/detector.py,sha256=JrZ7NGmG0vWb2Vskh-U_S1KYkyVjscUywwu2roe1HCQ,13665
|
|
3
|
+
epi_cli/__init__.py,sha256=KEh3YUH01d0w7B-56gEIgI87jHh-UDuIkxzVSfzl7y4,104
|
|
4
|
+
epi_cli/__main__.py,sha256=HzwyIuqH0lO6pJMApM8ZsXcNzmq9wIKtWM1zJvzVSzY,302
|
|
5
|
+
epi_cli/chat.py,sha256=D7ULAbciCPi_2rcGyoUmSYGGD0ceYYVcw4ekGSKoTQc,7400
|
|
6
|
+
epi_cli/debug.py,sha256=khGJ2xiohQYUgyTBJE5iZM-w1pKA0nou6VJPvmORKF4,3794
|
|
7
|
+
epi_cli/keys.py,sha256=3EZaNc-NvHNWWeHfxKTkZs3bUhOdw7sJ3X2_021__gE,9117
|
|
8
|
+
epi_cli/ls.py,sha256=ijFZdnzb8ncDFmNG7-j_mg7xLkXMJ_Cd_AwIE4tE7gI,5023
|
|
9
|
+
epi_cli/main.py,sha256=NXzJb2dt0R5zLIcaiy1yzdxbRcAbKWv1Oxr4tRZ4vBE,12366
|
|
10
|
+
epi_cli/record.py,sha256=bmUNr2cELwo6qVKbFvWlI2HpIbcGzXcM1MT2-Fs2cxI,7327
|
|
11
|
+
epi_cli/run.py,sha256=JHTL_sm1LN02IiaWBbM7vBwybzrrJbwsQsUxXg13fDY,14376
|
|
12
|
+
epi_cli/verify.py,sha256=9zr5gNH0v70Ngg_5F_JuFZQcUzWQ3YhH9WFlfUS1I0o,8244
|
|
13
|
+
epi_cli/view.py,sha256=EP9takENuZnRllBsxDze9Mm32TGsyxsQaUhlNmUNA_w,4027
|
|
14
|
+
epi_core/__init__.py,sha256=8CTVjxZDI6oy-MMqWTILY9h8rgSZXS8kVzgySympGJU,309
|
|
15
|
+
epi_core/container.py,sha256=Eop4CN3TgCoxRyEWorbjvVBnFaxS4zkccdDwgXQ4eIk,13344
|
|
16
|
+
epi_core/redactor.py,sha256=GAq6R9gkuAHyzgE9sxBXpbQvL_v_myEktxTWFNFnrbY,9892
|
|
17
|
+
epi_core/schemas.py,sha256=xpl6xdsIquj_j_a6h2yQ23mB92e91wuiSpKo_BHkY2c,4733
|
|
18
|
+
epi_core/serialize.py,sha256=KB7Z7dfDFh6qq0tlrwjWADOBUV4z32q29Dt2yiniGGg,5691
|
|
19
|
+
epi_core/storage.py,sha256=XEVbdr5xf00LDDJMqCdrZDFvVS-BZ1e1CWzDaJqG0jE,5374
|
|
20
|
+
epi_core/trust.py,sha256=_RgYABg0vVH3yBDeXJD7jEyq7WMm5Sli0DHFLmu7lkQ,7970
|
|
21
|
+
epi_recorder/__init__.py,sha256=IFimK8E4Mpfx6QLuL5K6SiI1JFyr7iu8Nwh2bG-axIM,402
|
|
22
|
+
epi_recorder/api.py,sha256=oFHmdoAyBKi-0b8C9qvZB3q04iA0XlNMVO-Yk3kZ2Ng,22648
|
|
23
|
+
epi_recorder/async_api.py,sha256=a2WQL8MnJ8uwnLD6unDZxASe5JbywP1V-8gcFyySFM8,4949
|
|
24
|
+
epi_recorder/bootstrap.py,sha256=vk6mKnaHcnanm8SB7dYGPDJ8E2iSBSX3OTQ3zyO-6b0,1851
|
|
25
|
+
epi_recorder/environment.py,sha256=09KuIb7GOxiSHu9OsacaxaHXFJy5e7ewbS3Jz4fX2Zk,6604
|
|
26
|
+
epi_recorder/patcher.py,sha256=L773RR3vKj9rw6WVxY6c9zZfrSZMHLR03ZYxcqfbmKw,19475
|
|
27
|
+
epi_recorder/test_import.py,sha256=_wrlfu0BLtT21AINf1_NugJTvM-RVNKJOyzokMezjO0,462
|
|
28
|
+
epi_recorder/test_script.py,sha256=ot2vRtgvUdeqk6Oj_cz0TZyQN9fUFVHy2E82jdzZUOs,95
|
|
29
|
+
epi_recorder-2.2.0.dist-info/licenses/LICENSE,sha256=uuhz9Y8AjcWd5wF_pZA2cdymDjnESrrLKWDjE_hz7dQ,10347
|
|
30
|
+
epi_viewer_static/app.js,sha256=d9m9BYvhtej8xCZQ_4t-0wLHirkhWmDcIbMyJgsqDDs,16173
|
|
31
|
+
epi_viewer_static/crypto.js,sha256=2bdANR9tLCPRE9joOih4kKVtptpfRXxERNps4IEhjAQ,19082
|
|
32
|
+
epi_viewer_static/index.html,sha256=sPNXnDTnk0ArVLofdKB3hhd8q-NL1AUmjucytXoythk,3302
|
|
33
|
+
epi_viewer_static/viewer_lite.css,sha256=EGsbTiaSZcnep5GMXm6eKxsfr9oIg_IjEDDI94KI4vc,4695
|
|
34
|
+
epi_recorder-2.2.0.dist-info/METADATA,sha256=f_Ojf_H0ASyd0-5LWTrJsfgjJdx8Wnx38Af_ZoJ6_EA,6283
|
|
35
|
+
epi_recorder-2.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
36
|
+
epi_recorder-2.2.0.dist-info/entry_points.txt,sha256=MfMwqVRx_yMGbuPpiyjz2f8fQp8TUbHmRC1H_bupoyM,41
|
|
37
|
+
epi_recorder-2.2.0.dist-info/top_level.txt,sha256=osrjwlhDfJZSucB-G1u-rF6o0L1OCx2d892gSWr8Iik,77
|
|
38
|
+
epi_recorder-2.2.0.dist-info/RECORD,,
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
modifications, and in Source or Object form, provided that You
|
|
92
92
|
meet the following conditions:
|
|
93
93
|
|
|
94
|
-
(a) You must give any other recipients of the Work or
|
|
95
|
-
|
|
94
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
95
|
+
Works a copy of this License; and
|
|
96
96
|
|
|
97
97
|
(b) You must cause any modified files to carry prominent notices
|
|
98
98
|
stating that You changed the files; and
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
other commercial damages or losses), even if such Contributor
|
|
163
163
|
has been advised of the possibility of such damages.
|
|
164
164
|
|
|
165
|
-
9. Accepting Warranty or Additional
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
166
|
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
167
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
168
|
or other liability obligations and/or rights consistent with this
|
|
@@ -173,29 +173,4 @@
|
|
|
173
173
|
incurred by, or claims asserted against, such Contributor by reason
|
|
174
174
|
of your accepting any such warranty or additional liability.
|
|
175
175
|
|
|
176
|
-
END OF TERMS AND CONDITIONS
|
|
177
|
-
|
|
178
|
-
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
-
|
|
180
|
-
To apply the Apache License to your work, attach the following
|
|
181
|
-
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
-
replaced with your own identifying information. (Don't include
|
|
183
|
-
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
-
comment syntax for the file format. We also recommend that a
|
|
185
|
-
file or class name and description of purpose be included on the
|
|
186
|
-
same "printed page" as the copyright notice for easier
|
|
187
|
-
identification within third-party archives.
|
|
188
|
-
|
|
189
|
-
Copyright 2024 EPI Project
|
|
190
|
-
|
|
191
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
-
you may not use this file except in compliance with the License.
|
|
193
|
-
You may obtain a copy of the License at
|
|
194
|
-
|
|
195
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
-
|
|
197
|
-
Unless required by applicable law or agreed to in writing, software
|
|
198
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
-
See the License for the specific language governing permissions and
|
|
201
|
-
limitations under the License.
|
|
176
|
+
END OF TERMS AND CONDITIONS
|