avenix 0.1.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.
- avenix/__init__.py +11 -0
- avenix/decorator.py +62 -0
- avenix/extractors.py +81 -0
- avenix/formatter.py +71 -0
- avenix/logger.py +29 -0
- avenix/models.py +97 -0
- avenix/tracer.py +215 -0
- avenix-0.1.0.dist-info/METADATA +247 -0
- avenix-0.1.0.dist-info/RECORD +12 -0
- avenix-0.1.0.dist-info/WHEEL +5 -0
- avenix-0.1.0.dist-info/licenses/LICENSE +21 -0
- avenix-0.1.0.dist-info/top_level.txt +1 -0
avenix/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Avenix - Python tracing library for AI requests.
|
|
3
|
+
|
|
4
|
+
Provides decorator-based tracing for AI/LLM requests with beautiful terminal output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .decorator import trace
|
|
8
|
+
from .tracer import Tracer
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["trace", "Tracer"]
|
avenix/decorator.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Trace decorator implementation
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
from typing import Callable, Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def trace(func: Callable) -> Callable:
|
|
9
|
+
"""
|
|
10
|
+
Decorator that traces AI request function execution.
|
|
11
|
+
|
|
12
|
+
Captures timing, model info, tokens, cost, prompt, and response.
|
|
13
|
+
Displays formatted trace to terminal after execution.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
func: The function to trace (should return AI response object)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Wrapped function with identical signature and return type
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
@trace
|
|
23
|
+
def call_openai(prompt: str):
|
|
24
|
+
return client.chat.completions.create(
|
|
25
|
+
model="gpt-4",
|
|
26
|
+
messages=[{"role": "user", "content": prompt}]
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
from .tracer import Tracer
|
|
30
|
+
|
|
31
|
+
# Create tracer instance once at decoration time
|
|
32
|
+
tracer = Tracer()
|
|
33
|
+
|
|
34
|
+
@functools.wraps(func)
|
|
35
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
36
|
+
# Record start time with high precision
|
|
37
|
+
start_time = time.perf_counter()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Execute the wrapped function
|
|
41
|
+
result = func(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
# Calculate latency
|
|
44
|
+
end_time = time.perf_counter()
|
|
45
|
+
latency = end_time - start_time
|
|
46
|
+
|
|
47
|
+
# Capture and display trace
|
|
48
|
+
tracer.capture_trace(
|
|
49
|
+
result=result,
|
|
50
|
+
latency=latency,
|
|
51
|
+
func_name=func.__name__
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Return original result unchanged
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
except Exception:
|
|
58
|
+
# Propagate exceptions without suppression
|
|
59
|
+
# No trace is captured on failure
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
return wrapper
|
avenix/extractors.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Provider-specific extraction logic for AI responses
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResponseExtractor(ABC):
|
|
11
|
+
"""Abstract base class for AI response extractors."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def can_extract(self, result: Any) -> bool:
|
|
15
|
+
"""Check if this extractor can handle the given result."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def extract(self, result: Any) -> Dict[str, Any]:
|
|
20
|
+
"""Extract trace data from result."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OpenAIExtractor(ResponseExtractor):
|
|
25
|
+
"""Extractor for OpenAI response format."""
|
|
26
|
+
|
|
27
|
+
def can_extract(self, result: Any) -> bool:
|
|
28
|
+
"""Check if result matches OpenAI format."""
|
|
29
|
+
usage = getattr(result, 'usage', None)
|
|
30
|
+
choices = getattr(result, 'choices', None)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
hasattr(result, 'model') and
|
|
34
|
+
hasattr(usage, 'prompt_tokens') and
|
|
35
|
+
hasattr(usage, 'completion_tokens') and
|
|
36
|
+
isinstance(choices, (list, tuple))
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def extract(self, result: Any) -> Dict[str, Any]:
|
|
40
|
+
"""Extract trace data from OpenAI response."""
|
|
41
|
+
try:
|
|
42
|
+
return {
|
|
43
|
+
'model': result.model,
|
|
44
|
+
'input_tokens': result.usage.prompt_tokens,
|
|
45
|
+
'output_tokens': result.usage.completion_tokens,
|
|
46
|
+
'prompt': '', # Not available in response
|
|
47
|
+
'response': result.choices[0].message.content
|
|
48
|
+
}
|
|
49
|
+
except (AttributeError, IndexError) as e:
|
|
50
|
+
logger.warning(f"Failed to extract OpenAI trace data: {e}")
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AnthropicExtractor(ResponseExtractor):
|
|
55
|
+
"""Extractor for Anthropic response format."""
|
|
56
|
+
|
|
57
|
+
def can_extract(self, result: Any) -> bool:
|
|
58
|
+
"""Check if result matches Anthropic format."""
|
|
59
|
+
usage = getattr(result, 'usage', None)
|
|
60
|
+
content = getattr(result, 'content', None)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
hasattr(result, 'model') and
|
|
64
|
+
hasattr(usage, 'input_tokens') and
|
|
65
|
+
hasattr(usage, 'output_tokens') and
|
|
66
|
+
isinstance(content, (list, tuple))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def extract(self, result: Any) -> Dict[str, Any]:
|
|
70
|
+
"""Extract trace data from Anthropic response."""
|
|
71
|
+
try:
|
|
72
|
+
return {
|
|
73
|
+
'model': result.model,
|
|
74
|
+
'input_tokens': result.usage.input_tokens,
|
|
75
|
+
'output_tokens': result.usage.output_tokens,
|
|
76
|
+
'prompt': '', # Not available in response
|
|
77
|
+
'response': result.content[0].text if result.content else ''
|
|
78
|
+
}
|
|
79
|
+
except (AttributeError, IndexError) as e:
|
|
80
|
+
logger.warning(f"Failed to extract Anthropic trace data: {e}")
|
|
81
|
+
return {}
|
avenix/formatter.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Terminal output formatting using rich library
|
|
2
|
+
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
from rich.console import RenderableType
|
|
6
|
+
|
|
7
|
+
from .models import TraceModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RichFormatter:
|
|
11
|
+
"""Formats traces using the rich library for beautiful terminal output."""
|
|
12
|
+
|
|
13
|
+
def format(self, trace: TraceModel) -> RenderableType:
|
|
14
|
+
"""
|
|
15
|
+
Format a trace model for terminal display.
|
|
16
|
+
|
|
17
|
+
Creates a structured panel with:
|
|
18
|
+
- Header with emoji and title
|
|
19
|
+
- Metadata section (model, latency, tokens, cost)
|
|
20
|
+
- Prompt section
|
|
21
|
+
- Response section
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
trace: The trace model to format
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Rich renderable object (Panel)
|
|
28
|
+
"""
|
|
29
|
+
# Build metadata section
|
|
30
|
+
metadata = Text()
|
|
31
|
+
metadata.append("Model: ", style="bold cyan")
|
|
32
|
+
metadata.append(f"{trace.model}\n")
|
|
33
|
+
|
|
34
|
+
metadata.append("Latency: ", style="bold cyan")
|
|
35
|
+
metadata.append(f"{trace.latency:.2f}s\n")
|
|
36
|
+
|
|
37
|
+
metadata.append("Input: ", style="bold cyan")
|
|
38
|
+
metadata.append(f"{trace.input_tokens} tokens\n")
|
|
39
|
+
|
|
40
|
+
metadata.append("Output: ", style="bold cyan")
|
|
41
|
+
metadata.append(f"{trace.output_tokens} tokens\n")
|
|
42
|
+
|
|
43
|
+
metadata.append("Cost: ", style="bold cyan")
|
|
44
|
+
metadata.append(f"${trace.cost:.4f}\n")
|
|
45
|
+
|
|
46
|
+
# Build complete output
|
|
47
|
+
output = Text()
|
|
48
|
+
output.append(metadata)
|
|
49
|
+
output.append("\n")
|
|
50
|
+
|
|
51
|
+
# Prompt section
|
|
52
|
+
output.append("━" * 50 + "\n", style="dim")
|
|
53
|
+
output.append("Prompt\n", style="bold yellow")
|
|
54
|
+
output.append("━" * 50 + "\n", style="dim")
|
|
55
|
+
output.append(f"{trace.prompt}\n\n")
|
|
56
|
+
|
|
57
|
+
# Response section
|
|
58
|
+
output.append("━" * 50 + "\n", style="dim")
|
|
59
|
+
output.append("Response\n", style="bold green")
|
|
60
|
+
output.append("━" * 50 + "\n", style="dim")
|
|
61
|
+
output.append(f"{trace.response}\n")
|
|
62
|
+
|
|
63
|
+
# Wrap in panel with title
|
|
64
|
+
panel = Panel(
|
|
65
|
+
output,
|
|
66
|
+
title="🚀 Avenix Trace",
|
|
67
|
+
border_style="blue",
|
|
68
|
+
padding=(1, 2)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return panel
|
avenix/logger.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Terminal display logger
|
|
2
|
+
|
|
3
|
+
from rich.console import Console, RenderableType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RichLogger:
|
|
7
|
+
"""Logs traces to terminal using rich library."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, console: Console = None):
|
|
10
|
+
"""
|
|
11
|
+
Initialize logger with optional custom console.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
console: Custom rich Console instance
|
|
15
|
+
"""
|
|
16
|
+
self.console = console or Console()
|
|
17
|
+
|
|
18
|
+
def log(self, renderable: RenderableType) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Display a formatted trace to the terminal.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
renderable: Rich renderable object to display
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
self.console.print(renderable)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
# Fallback to basic print if rich rendering fails
|
|
29
|
+
print(f"[Avenix] Failed to render trace: {e}")
|
avenix/models.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Data models for AgentForge tracing
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TraceModel(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Data model for a single AI request trace.
|
|
9
|
+
|
|
10
|
+
All fields are validated by Pydantic for type safety.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
model: str = Field(
|
|
14
|
+
description="Name of the AI model used (e.g., 'gpt-4', 'claude-3')"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
latency: float = Field(
|
|
18
|
+
ge=0.0,
|
|
19
|
+
description="Request latency in seconds"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
input_tokens: int = Field(
|
|
23
|
+
ge=0,
|
|
24
|
+
description="Number of tokens in the input/prompt"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
output_tokens: int = Field(
|
|
28
|
+
ge=0,
|
|
29
|
+
description="Number of tokens in the output/response"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
cost: float = Field(
|
|
33
|
+
ge=0.0,
|
|
34
|
+
description="Request cost in USD"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
prompt: str = Field(
|
|
38
|
+
default="",
|
|
39
|
+
description="Input prompt text"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
response: str = Field(
|
|
43
|
+
default="",
|
|
44
|
+
description="Output response text"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@field_validator('latency')
|
|
48
|
+
@classmethod
|
|
49
|
+
def round_latency(cls, v: float) -> float:
|
|
50
|
+
"""Round latency to 2 decimal places."""
|
|
51
|
+
return round(v, 2)
|
|
52
|
+
|
|
53
|
+
@field_validator('cost')
|
|
54
|
+
@classmethod
|
|
55
|
+
def round_cost(cls, v: float) -> float:
|
|
56
|
+
"""Round cost to 4 decimal places."""
|
|
57
|
+
return round(v, 4)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Model pricing table (prices per 1K tokens in USD)
|
|
61
|
+
MODEL_PRICING = {
|
|
62
|
+
# OpenAI models
|
|
63
|
+
"gpt-4": {"input": 0.03, "output": 0.06},
|
|
64
|
+
"gpt-4-turbo": {"input": 0.01, "output": 0.03},
|
|
65
|
+
"gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
|
|
66
|
+
|
|
67
|
+
# Anthropic models
|
|
68
|
+
"claude-3-opus": {"input": 0.015, "output": 0.075},
|
|
69
|
+
"claude-3-sonnet": {"input": 0.003, "output": 0.015},
|
|
70
|
+
"claude-3-haiku": {"input": 0.00025, "output": 0.00125},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
|
|
75
|
+
"""
|
|
76
|
+
Calculate cost in USD based on model and token counts.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
model: Name of the AI model (e.g., 'gpt-4', 'claude-3-opus')
|
|
80
|
+
input_tokens: Number of input/prompt tokens
|
|
81
|
+
output_tokens: Number of output/response tokens
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Cost in USD, rounded to 4 decimal places
|
|
85
|
+
Returns 0.0 for unknown models
|
|
86
|
+
|
|
87
|
+
Formula:
|
|
88
|
+
(input_tokens / 1000) * input_price + (output_tokens / 1000) * output_price
|
|
89
|
+
"""
|
|
90
|
+
if model not in MODEL_PRICING:
|
|
91
|
+
return 0.0
|
|
92
|
+
|
|
93
|
+
pricing = MODEL_PRICING[model]
|
|
94
|
+
input_cost = (input_tokens / 1000) * pricing["input"]
|
|
95
|
+
output_cost = (output_tokens / 1000) * pricing["output"]
|
|
96
|
+
|
|
97
|
+
return round(input_cost + output_cost, 4)
|
avenix/tracer.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Core tracer implementation
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from .models import TraceModel, calculate_cost
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
_TRACE_MODEL_CLASS = TraceModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_fallback_trace(
|
|
16
|
+
model: str = 'unknown',
|
|
17
|
+
latency: float = 0.0,
|
|
18
|
+
input_tokens: int = 0,
|
|
19
|
+
output_tokens: int = 0,
|
|
20
|
+
cost: float = 0.0,
|
|
21
|
+
prompt: str = '',
|
|
22
|
+
response: str = '',
|
|
23
|
+
):
|
|
24
|
+
"""Return a sanitized trace model, falling back to a simple object if needed."""
|
|
25
|
+
sanitized = {
|
|
26
|
+
'model': model or 'unknown',
|
|
27
|
+
'latency': max(0.0, latency or 0.0),
|
|
28
|
+
'input_tokens': max(0, input_tokens or 0),
|
|
29
|
+
'output_tokens': max(0, output_tokens or 0),
|
|
30
|
+
'cost': max(0.0, cost or 0.0),
|
|
31
|
+
'prompt': prompt or '',
|
|
32
|
+
'response': response or '',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
return _TRACE_MODEL_CLASS(**sanitized)
|
|
37
|
+
except Exception:
|
|
38
|
+
return SimpleNamespace(**sanitized)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Tracer:
|
|
42
|
+
"""Core tracing engine for Avenix."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, logger=None, formatter=None):
|
|
45
|
+
"""
|
|
46
|
+
Initialize tracer with optional custom logger and formatter.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
logger: Custom logger instance (defaults to RichLogger)
|
|
50
|
+
formatter: Custom formatter instance (defaults to RichFormatter)
|
|
51
|
+
"""
|
|
52
|
+
# Lazy import to avoid circular dependencies
|
|
53
|
+
if logger is None:
|
|
54
|
+
from .logger import RichLogger
|
|
55
|
+
self.logger = RichLogger()
|
|
56
|
+
else:
|
|
57
|
+
self.logger = logger
|
|
58
|
+
|
|
59
|
+
if formatter is None:
|
|
60
|
+
from .formatter import RichFormatter
|
|
61
|
+
self.formatter = RichFormatter()
|
|
62
|
+
else:
|
|
63
|
+
self.formatter = formatter
|
|
64
|
+
|
|
65
|
+
def capture_trace(
|
|
66
|
+
self,
|
|
67
|
+
result: Any,
|
|
68
|
+
latency: float,
|
|
69
|
+
func_name: Optional[str] = None
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Capture and display a trace from function execution result.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
result: Return value from traced function
|
|
76
|
+
latency: Execution time in seconds
|
|
77
|
+
func_name: Optional name of traced function
|
|
78
|
+
"""
|
|
79
|
+
# Extract trace data from result
|
|
80
|
+
extracted = self._extract_trace_data(result)
|
|
81
|
+
|
|
82
|
+
# Calculate cost
|
|
83
|
+
cost = calculate_cost(
|
|
84
|
+
extracted.get('model', 'unknown'),
|
|
85
|
+
extracted.get('input_tokens', 0),
|
|
86
|
+
extracted.get('output_tokens', 0)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Create validated trace model
|
|
91
|
+
trace = TraceModel(
|
|
92
|
+
model=extracted.get('model', 'unknown'),
|
|
93
|
+
latency=latency,
|
|
94
|
+
input_tokens=extracted.get('input_tokens', 0),
|
|
95
|
+
output_tokens=extracted.get('output_tokens', 0),
|
|
96
|
+
cost=cost,
|
|
97
|
+
prompt=extracted.get('prompt', ''),
|
|
98
|
+
response=extracted.get('response', '')
|
|
99
|
+
)
|
|
100
|
+
except ValidationError as e:
|
|
101
|
+
logger.error(f"Trace validation failed: {e}")
|
|
102
|
+
trace = _build_fallback_trace(
|
|
103
|
+
model='unknown',
|
|
104
|
+
latency=latency,
|
|
105
|
+
response='[Validation failed]',
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Unexpected error creating trace: {e}")
|
|
109
|
+
trace = _build_fallback_trace(
|
|
110
|
+
model='unknown',
|
|
111
|
+
latency=latency,
|
|
112
|
+
response='[Validation failed]',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Display the trace
|
|
116
|
+
self._display_trace(trace)
|
|
117
|
+
|
|
118
|
+
def create_trace(
|
|
119
|
+
self,
|
|
120
|
+
model: str,
|
|
121
|
+
latency: float,
|
|
122
|
+
input_tokens: int,
|
|
123
|
+
output_tokens: int,
|
|
124
|
+
cost: float,
|
|
125
|
+
prompt: str,
|
|
126
|
+
response: str
|
|
127
|
+
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Manually create and display a trace with explicit values.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
model: Name of the AI model used
|
|
133
|
+
latency: Request latency in seconds
|
|
134
|
+
input_tokens: Number of input tokens
|
|
135
|
+
output_tokens: Number of output tokens
|
|
136
|
+
cost: Request cost in dollars
|
|
137
|
+
prompt: Input prompt text
|
|
138
|
+
response: Response text from AI
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
trace = TraceModel(
|
|
142
|
+
model=model,
|
|
143
|
+
latency=latency,
|
|
144
|
+
input_tokens=input_tokens,
|
|
145
|
+
output_tokens=output_tokens,
|
|
146
|
+
cost=cost,
|
|
147
|
+
prompt=prompt,
|
|
148
|
+
response=response
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Trace creation failed: {e}")
|
|
152
|
+
trace = _build_fallback_trace(
|
|
153
|
+
model=model,
|
|
154
|
+
latency=latency,
|
|
155
|
+
input_tokens=input_tokens,
|
|
156
|
+
output_tokens=output_tokens,
|
|
157
|
+
cost=cost,
|
|
158
|
+
prompt=prompt,
|
|
159
|
+
response=response,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._display_trace(trace)
|
|
163
|
+
|
|
164
|
+
def _extract_trace_data(self, result: Any) -> dict:
|
|
165
|
+
"""
|
|
166
|
+
Extract trace data using chain of extractors.
|
|
167
|
+
|
|
168
|
+
Returns dict with partial or complete trace data.
|
|
169
|
+
Missing fields will use default values.
|
|
170
|
+
"""
|
|
171
|
+
# Lazy import extractors
|
|
172
|
+
from .extractors import OpenAIExtractor, AnthropicExtractor
|
|
173
|
+
|
|
174
|
+
extractors = [
|
|
175
|
+
OpenAIExtractor(),
|
|
176
|
+
AnthropicExtractor(),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
for extractor in extractors:
|
|
181
|
+
if extractor.can_extract(result):
|
|
182
|
+
data = extractor.extract(result)
|
|
183
|
+
if data: # Extractor returned something
|
|
184
|
+
return data
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(f"Extraction failed: {e}", exc_info=True)
|
|
187
|
+
|
|
188
|
+
# Fallback to minimal data if no extractor matched
|
|
189
|
+
logger.warning(
|
|
190
|
+
f"No extractor found for result type: {type(result).__name__}"
|
|
191
|
+
)
|
|
192
|
+
return {
|
|
193
|
+
'model': 'unknown',
|
|
194
|
+
'input_tokens': 0,
|
|
195
|
+
'output_tokens': 0,
|
|
196
|
+
'prompt': '',
|
|
197
|
+
'response': str(result)[:500] # Fallback to string repr
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
def _display_trace(self, trace: TraceModel) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Display a trace to the terminal.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
trace: The trace model to display
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
formatted = self.formatter.format(trace)
|
|
209
|
+
self.logger.log(formatted)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Failed to display trace: {e}", exc_info=True)
|
|
212
|
+
# Fallback to basic output
|
|
213
|
+
print(f"[Avenix] Model: {trace.model}, "
|
|
214
|
+
f"Latency: {trace.latency:.2f}s, "
|
|
215
|
+
f"Cost: ${trace.cost:.4f}")
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: avenix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Avenix is a focused Python tracing library for AI requests with beautiful terminal output
|
|
5
|
+
Author: Avenix Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/avenix/avenix
|
|
8
|
+
Project-URL: Repository, https://github.com/avenix/avenix
|
|
9
|
+
Project-URL: Documentation, https://github.com/avenix/avenix#readme
|
|
10
|
+
Keywords: ai,tracing,llm,monitoring,openai,anthropic
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Monitoring
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
23
|
+
Requires-Dist: rich<14.0,>=13.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: hypothesis<7.0,>=6.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov<5.0,>=4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-mock<4.0,>=3.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Avenix v0.1
|
|
32
|
+
|
|
33
|
+
A Python tracing library for AI/LLM requests with beautiful terminal output.
|
|
34
|
+
|
|
35
|
+
Avenix provides a decorator-based API for tracing AI model requests, automatically capturing execution metrics like timing, token usage, and costs, then displaying them in richly formatted terminal output.
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
|
|
39
|
+
Avenix simplifies monitoring AI/LLM requests by:
|
|
40
|
+
- **Automatic Capture**: Uses a simple `@trace` decorator to automatically capture request metrics
|
|
41
|
+
- **Beautiful Display**: Renders trace information in a formatted terminal panel with colors and separators
|
|
42
|
+
- **Multi-Provider Support**: Works with OpenAI and Anthropic model responses out of the box
|
|
43
|
+
- **Cost Tracking**: Automatically calculates request costs based on model pricing
|
|
44
|
+
- **Extensible**: Easy to add custom extractors for new AI providers
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install avenix
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Python 3.11+
|
|
55
|
+
- pydantic ^2.0
|
|
56
|
+
- rich ^13.0
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### Using the @trace Decorator
|
|
61
|
+
|
|
62
|
+
The simplest way to use Avenix is with the `@trace` decorator:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from avenix import trace
|
|
66
|
+
from openai import OpenAI
|
|
67
|
+
|
|
68
|
+
client = OpenAI()
|
|
69
|
+
|
|
70
|
+
@trace
|
|
71
|
+
def get_gpt_response(prompt: str):
|
|
72
|
+
"""Call GPT-4 with the given prompt."""
|
|
73
|
+
response = client.chat.completions.create(
|
|
74
|
+
model="gpt-4",
|
|
75
|
+
messages=[{"role": "user", "content": prompt}]
|
|
76
|
+
)
|
|
77
|
+
return response
|
|
78
|
+
|
|
79
|
+
# When you call the function, Avenix will automatically:
|
|
80
|
+
# 1. Measure execution time
|
|
81
|
+
# 2. Extract model, tokens, and response from the result
|
|
82
|
+
# 3. Calculate cost based on token usage
|
|
83
|
+
# 4. Display formatted trace output to terminal
|
|
84
|
+
result = get_gpt_response("What is machine learning?")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Manual Trace Creation
|
|
88
|
+
|
|
89
|
+
For more control, you can manually create traces using the `Tracer` API:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from avenix import Tracer
|
|
93
|
+
|
|
94
|
+
tracer = Tracer()
|
|
95
|
+
|
|
96
|
+
# Later, manually create a trace with explicit values
|
|
97
|
+
tracer.create_trace(
|
|
98
|
+
model="gpt-4",
|
|
99
|
+
latency=2.5,
|
|
100
|
+
input_tokens=150,
|
|
101
|
+
output_tokens=300,
|
|
102
|
+
cost=0.045,
|
|
103
|
+
prompt="What is AI?",
|
|
104
|
+
response="AI is artificial intelligence..."
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Supported Models
|
|
109
|
+
|
|
110
|
+
Avenix includes built-in support for:
|
|
111
|
+
|
|
112
|
+
### OpenAI
|
|
113
|
+
- gpt-4
|
|
114
|
+
- gpt-4-turbo
|
|
115
|
+
- gpt-3.5-turbo
|
|
116
|
+
|
|
117
|
+
### Anthropic
|
|
118
|
+
- claude-3-opus
|
|
119
|
+
- claude-3-sonnet
|
|
120
|
+
- claude-3-haiku
|
|
121
|
+
|
|
122
|
+
## Features in v0.1
|
|
123
|
+
|
|
124
|
+
✅ Decorator-based tracing API
|
|
125
|
+
✅ Automatic timing measurement with perf_counter
|
|
126
|
+
✅ OpenAI and Anthropic response extraction
|
|
127
|
+
✅ Model pricing table and cost calculation
|
|
128
|
+
✅ Beautiful terminal output with rich formatting
|
|
129
|
+
✅ Error handling and graceful fallbacks
|
|
130
|
+
✅ Property-based test suite for correctness verification
|
|
131
|
+
|
|
132
|
+
## Out of Scope for v0.1
|
|
133
|
+
|
|
134
|
+
The following features are planned for future releases:
|
|
135
|
+
|
|
136
|
+
- Custom formatter plugins
|
|
137
|
+
- Database/file persistence of traces
|
|
138
|
+
- Trace filtering and search
|
|
139
|
+
- Performance statistics aggregation
|
|
140
|
+
- Integration with external logging services
|
|
141
|
+
- Support for additional AI providers
|
|
142
|
+
- Rate limiting and quota management
|
|
143
|
+
- Async/await support
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
### API Reference
|
|
148
|
+
|
|
149
|
+
#### @trace Decorator
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
@trace
|
|
153
|
+
def your_function():
|
|
154
|
+
# ... your code that calls an AI model
|
|
155
|
+
return response
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The `@trace` decorator:
|
|
159
|
+
- Measures execution time with `time.perf_counter()`
|
|
160
|
+
- Captures the function result
|
|
161
|
+
- Calls the global `Tracer` instance to extract data and display trace
|
|
162
|
+
- Propagates exceptions without suppression
|
|
163
|
+
- Preserves the original function's return value and metadata
|
|
164
|
+
|
|
165
|
+
#### Tracer Class
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from avenix import Tracer
|
|
169
|
+
|
|
170
|
+
tracer = Tracer(logger=None, formatter=None)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Methods:
|
|
174
|
+
- `capture_trace(result, latency, func_name=None)`: Capture and display a trace from function execution
|
|
175
|
+
- `create_trace(model, latency, input_tokens, output_tokens, cost, prompt, response)`: Manually create and display a trace
|
|
176
|
+
|
|
177
|
+
### TraceModel
|
|
178
|
+
|
|
179
|
+
The `TraceModel` class represents a single trace with validation:
|
|
180
|
+
|
|
181
|
+
Fields:
|
|
182
|
+
- `model` (str): Name of the AI model
|
|
183
|
+
- `latency` (float): Execution time in seconds (rounded to 2 decimals)
|
|
184
|
+
- `input_tokens` (int): Number of input tokens (must be non-negative)
|
|
185
|
+
- `output_tokens` (int): Number of output tokens (must be non-negative)
|
|
186
|
+
- `cost` (float): Request cost in dollars (rounded to 4 decimals, must be non-negative)
|
|
187
|
+
- `prompt` (str): Input prompt text (defaults to empty string)
|
|
188
|
+
- `response` (str): Model response text (defaults to empty string)
|
|
189
|
+
|
|
190
|
+
## Examples
|
|
191
|
+
|
|
192
|
+
See the `examples/` directory for complete working examples:
|
|
193
|
+
|
|
194
|
+
- `openai_example.py`: Using @trace with OpenAI API
|
|
195
|
+
- `anthropic_example.py`: Using @trace with Anthropic API
|
|
196
|
+
- `manual_trace.py`: Creating traces manually with Tracer API
|
|
197
|
+
|
|
198
|
+
## Testing
|
|
199
|
+
|
|
200
|
+
Avenix includes a comprehensive test suite with property-based tests:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
pytest tests/ -v # Run all tests
|
|
204
|
+
pytest tests/ --cov # Run with coverage report
|
|
205
|
+
pytest tests/test_models.py -v # Run specific test file
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Test coverage targets:
|
|
209
|
+
- Core modules (decorator, tracer, models, formatter, extractors): >90%
|
|
210
|
+
- Property-based tests for 16 correctness properties
|
|
211
|
+
- Unit tests for all feature components
|
|
212
|
+
|
|
213
|
+
## Architecture
|
|
214
|
+
|
|
215
|
+
Avenix follows a layered architecture:
|
|
216
|
+
|
|
217
|
+
1. **Models Layer** (`models.py`): Defines `TraceModel` with Pydantic validation
|
|
218
|
+
2. **Decorator Layer** (`decorator.py`): Provides `@trace` decorator for wrapping functions
|
|
219
|
+
3. **Tracer Layer** (`tracer.py`): Core orchestration with optional custom logger/formatter
|
|
220
|
+
4. **Extraction Layer** (`extractors.py`): Provider-specific response extractors
|
|
221
|
+
5. **Formatting Layer** (`formatter.py`): Beautiful terminal output with rich library
|
|
222
|
+
6. **Logging Layer** (`logger.py`): Terminal display with fallback handling
|
|
223
|
+
|
|
224
|
+
## Error Handling
|
|
225
|
+
|
|
226
|
+
Avenix is designed to be resilient to errors:
|
|
227
|
+
|
|
228
|
+
- **Extraction Failures**: If response format doesn't match known providers, uses sensible defaults
|
|
229
|
+
- **Validation Failures**: If TraceModel validation fails, falls back to defaults
|
|
230
|
+
- **Rendering Failures**: If rich formatting fails, falls back to basic print output
|
|
231
|
+
- **Exception Propagation**: Errors in traced functions propagate normally without suppression
|
|
232
|
+
|
|
233
|
+
## Contributing
|
|
234
|
+
|
|
235
|
+
This is a v0.1 release. Feedback and contributions are welcome!
|
|
236
|
+
|
|
237
|
+
## API Reference
|
|
238
|
+
|
|
239
|
+
For detailed API documentation, see [API.md](API.md).
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT License - See LICENSE file for details
|
|
244
|
+
|
|
245
|
+
## Changelog
|
|
246
|
+
|
|
247
|
+
See CHANGELOG.md for version history and release notes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
avenix/__init__.py,sha256=As2hvX77otSC2pVaVBynLQVCAMjPr-Qde3663Ex-Bsk,253
|
|
2
|
+
avenix/decorator.py,sha256=O-3vqdNf7q1XP6abcY-roe349BYsepMAWrbo_RVjXXY,1742
|
|
3
|
+
avenix/extractors.py,sha256=j4ckpYEV8FqcbdFNaoyEtzs1kXH9bTru4yex0t-phOQ,2778
|
|
4
|
+
avenix/formatter.py,sha256=5bsRLGFvM1MdwxTQ7a-EWv3lAvGaTJO0wTUTxsG6_Vg,2235
|
|
5
|
+
avenix/logger.py,sha256=lY5uo9Nw3_KBap2_pARCuaWKYRwjlLGNK55z_UW8P30,825
|
|
6
|
+
avenix/models.py,sha256=3IR58kcD7knHFZ7xZ7QeNerLMyIYFaEBLhjfGpD9Mow,2608
|
|
7
|
+
avenix/tracer.py,sha256=SHF7k8WPDsMBDRUAfOhlBP4XW1f4dVRA6zNTYOUgHYE,6766
|
|
8
|
+
avenix-0.1.0.dist-info/licenses/LICENSE,sha256=H3GuE_5sx3TdFZP1Pmsyhk1hARuUaVectoYcPwF9O58,1080
|
|
9
|
+
avenix-0.1.0.dist-info/METADATA,sha256=XtcuXrJiYXNCQSYNYnZlNCzLeO-onCdALNy8y0HyP1Y,7507
|
|
10
|
+
avenix-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
avenix-0.1.0.dist-info/top_level.txt,sha256=jc9_k43DzwCAngJZNa31azg1h94chC4uM1ZDAwaMhCM,7
|
|
12
|
+
avenix-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 AgentForge Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
avenix
|