auditi 0.1.0__py3-none-any.whl → 0.1.1__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.
- auditi/__init__.py +4 -0
- auditi/decorators.py +42 -41
- auditi/instrumentation.py +105 -0
- auditi/types/api_types.py +12 -1
- {auditi-0.1.0.dist-info → auditi-0.1.1.dist-info}/METADATA +1 -1
- {auditi-0.1.0.dist-info → auditi-0.1.1.dist-info}/RECORD +8 -7
- {auditi-0.1.0.dist-info → auditi-0.1.1.dist-info}/WHEEL +0 -0
- {auditi-0.1.0.dist-info → auditi-0.1.1.dist-info}/licenses/LICENSE +0 -0
auditi/__init__.py
CHANGED
|
@@ -15,6 +15,8 @@ from .transport import BaseTransport, SyncHttpTransport, DebugTransport
|
|
|
15
15
|
from .types import TraceInput, SpanInput, EvaluationResult
|
|
16
16
|
from .events import EventType, StreamEvent
|
|
17
17
|
|
|
18
|
+
from .instrumentation import instrument
|
|
19
|
+
|
|
18
20
|
__version__ = "0.1.0"
|
|
19
21
|
|
|
20
22
|
__all__ = [
|
|
@@ -22,6 +24,8 @@ __all__ = [
|
|
|
22
24
|
"init",
|
|
23
25
|
"get_client",
|
|
24
26
|
"AuditiClient",
|
|
27
|
+
# Instrumentation
|
|
28
|
+
"instrument",
|
|
25
29
|
# Context
|
|
26
30
|
"set_context",
|
|
27
31
|
"get_context",
|
auditi/decorators.py
CHANGED
|
@@ -127,6 +127,33 @@ def _apply_usage_to_trace(trace: TraceInput, usage: Any, model: Optional[str] =
|
|
|
127
127
|
trace.cost = (trace.cost or 0.0) + incremental_cost
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
def _ensure_str_input(value: Any) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Convert various input formats to a plain string for user_input.
|
|
133
|
+
|
|
134
|
+
Handles:
|
|
135
|
+
- str: returned as-is
|
|
136
|
+
- list of message dicts (OpenAI/chat format): extracts last user message content
|
|
137
|
+
- dict with 'content' or 'message' key: extracts the value
|
|
138
|
+
- anything else: str() conversion
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(value, str):
|
|
141
|
+
return value
|
|
142
|
+
if isinstance(value, list):
|
|
143
|
+
# Try to extract the last user message from chat-format messages
|
|
144
|
+
for msg in reversed(value):
|
|
145
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
146
|
+
content = msg.get("content", "")
|
|
147
|
+
return str(content) if not isinstance(content, str) else content
|
|
148
|
+
# Fallback: stringify the list
|
|
149
|
+
return str(value)
|
|
150
|
+
if isinstance(value, dict):
|
|
151
|
+
return value.get("content") or value.get("message") or str(value)
|
|
152
|
+
if value is None:
|
|
153
|
+
return ""
|
|
154
|
+
return str(value)
|
|
155
|
+
|
|
156
|
+
|
|
130
157
|
def _execute_as_standalone_trace(
|
|
131
158
|
func: Callable,
|
|
132
159
|
args: tuple,
|
|
@@ -151,19 +178,11 @@ def _execute_as_standalone_trace(
|
|
|
151
178
|
# Extract user input from args/kwargs
|
|
152
179
|
user_input = ""
|
|
153
180
|
if args:
|
|
154
|
-
|
|
155
|
-
if isinstance(first_arg, str):
|
|
156
|
-
user_input = first_arg
|
|
157
|
-
elif isinstance(first_arg, list):
|
|
158
|
-
# Could be messages array
|
|
159
|
-
user_input = str(first_arg)
|
|
160
|
-
elif isinstance(first_arg, dict):
|
|
161
|
-
user_input = first_arg.get("content") or first_arg.get("message") or str(first_arg)
|
|
162
|
-
else:
|
|
163
|
-
user_input = str(first_arg)
|
|
181
|
+
user_input = _ensure_str_input(args[0])
|
|
164
182
|
|
|
165
183
|
if not user_input:
|
|
166
|
-
|
|
184
|
+
raw = kwargs.get("prompt") or kwargs.get("message") or kwargs.get("messages") or kwargs.get("query") or ""
|
|
185
|
+
user_input = _ensure_str_input(raw)
|
|
167
186
|
|
|
168
187
|
# Create trace
|
|
169
188
|
trace = TraceInput(
|
|
@@ -352,25 +371,19 @@ def trace_agent(
|
|
|
352
371
|
|
|
353
372
|
# Get the actual user input from the correct position
|
|
354
373
|
if len(args) > start_idx:
|
|
355
|
-
|
|
356
|
-
if isinstance(first_arg, str):
|
|
357
|
-
user_input = first_arg
|
|
358
|
-
elif isinstance(first_arg, dict) and "message" in first_arg:
|
|
359
|
-
user_input = first_arg["message"]
|
|
360
|
-
elif isinstance(first_arg, dict) and "content" in first_arg:
|
|
361
|
-
user_input = first_arg["content"]
|
|
362
|
-
else:
|
|
363
|
-
user_input = str(first_arg)
|
|
374
|
+
user_input = _ensure_str_input(args[start_idx])
|
|
364
375
|
|
|
365
376
|
# Also check for user_input/message/query in kwargs
|
|
366
377
|
if not user_input:
|
|
367
|
-
|
|
378
|
+
raw = (
|
|
368
379
|
kwargs.get("user_input")
|
|
369
380
|
or kwargs.get("message")
|
|
381
|
+
or kwargs.get("messages")
|
|
370
382
|
or kwargs.get("query")
|
|
371
383
|
or kwargs.get("prompt")
|
|
372
384
|
or ""
|
|
373
385
|
)
|
|
386
|
+
user_input = _ensure_str_input(raw)
|
|
374
387
|
|
|
375
388
|
_debug_log(f"Captured user input:", {"user_input": user_input[:200]})
|
|
376
389
|
|
|
@@ -544,25 +557,19 @@ def trace_agent(
|
|
|
544
557
|
|
|
545
558
|
# Get the actual user input from the correct position
|
|
546
559
|
if len(args) > start_idx:
|
|
547
|
-
|
|
548
|
-
if isinstance(first_arg, str):
|
|
549
|
-
user_input = first_arg
|
|
550
|
-
elif isinstance(first_arg, dict) and "message" in first_arg:
|
|
551
|
-
user_input = first_arg["message"]
|
|
552
|
-
elif isinstance(first_arg, dict) and "content" in first_arg:
|
|
553
|
-
user_input = first_arg["content"]
|
|
554
|
-
else:
|
|
555
|
-
user_input = str(first_arg)
|
|
560
|
+
user_input = _ensure_str_input(args[start_idx])
|
|
556
561
|
|
|
557
562
|
# Also check for user_input/message/query in kwargs
|
|
558
563
|
if not user_input:
|
|
559
|
-
|
|
564
|
+
raw = (
|
|
560
565
|
kwargs.get("user_input")
|
|
561
566
|
or kwargs.get("message")
|
|
567
|
+
or kwargs.get("messages")
|
|
562
568
|
or kwargs.get("query")
|
|
563
569
|
or kwargs.get("prompt")
|
|
564
570
|
or ""
|
|
565
571
|
)
|
|
572
|
+
user_input = _ensure_str_input(raw)
|
|
566
573
|
|
|
567
574
|
_debug_log(f"Captured user input:", {"user_input": user_input[:200]})
|
|
568
575
|
|
|
@@ -747,25 +754,19 @@ def trace_agent(
|
|
|
747
754
|
|
|
748
755
|
# Get the actual user input from the correct position
|
|
749
756
|
if len(args) > start_idx:
|
|
750
|
-
|
|
751
|
-
if isinstance(first_arg, str):
|
|
752
|
-
user_input = first_arg
|
|
753
|
-
elif isinstance(first_arg, dict) and "message" in first_arg:
|
|
754
|
-
user_input = first_arg["message"]
|
|
755
|
-
elif isinstance(first_arg, dict) and "content" in first_arg:
|
|
756
|
-
user_input = first_arg["content"]
|
|
757
|
-
else:
|
|
758
|
-
user_input = str(first_arg)
|
|
757
|
+
user_input = _ensure_str_input(args[start_idx])
|
|
759
758
|
|
|
760
759
|
# Also check for user_input/message/query in kwargs
|
|
761
760
|
if not user_input:
|
|
762
|
-
|
|
761
|
+
raw = (
|
|
763
762
|
kwargs.get("user_input")
|
|
764
763
|
or kwargs.get("message")
|
|
764
|
+
or kwargs.get("messages")
|
|
765
765
|
or kwargs.get("query")
|
|
766
766
|
or kwargs.get("prompt")
|
|
767
767
|
or ""
|
|
768
768
|
)
|
|
769
|
+
user_input = _ensure_str_input(raw)
|
|
769
770
|
|
|
770
771
|
_debug_log(f"Captured user input:", {"user_input": user_input[:200]})
|
|
771
772
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import functools
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from .decorators import trace_llm
|
|
7
|
+
from .client import get_client
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("auditi.instrumentation")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def instrument(
|
|
13
|
+
openai: bool = True,
|
|
14
|
+
anthropic: bool = True,
|
|
15
|
+
langchain: bool = False, # Placeholder for future
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Automatically instrument installed libraries to capture traces.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
openai: Whether to instrument the OpenAI library
|
|
22
|
+
anthropic: Whether to instrument the Anthropic library
|
|
23
|
+
langchain: Whether to instrument LangChain (not yet implemented)
|
|
24
|
+
"""
|
|
25
|
+
if openai:
|
|
26
|
+
_instrument_openai()
|
|
27
|
+
|
|
28
|
+
if anthropic:
|
|
29
|
+
_instrument_anthropic()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _instrument_openai():
|
|
33
|
+
"""Patch OpenAI client methods"""
|
|
34
|
+
try:
|
|
35
|
+
import openai
|
|
36
|
+
from openai import OpenAI, AsyncOpenAI
|
|
37
|
+
from openai.resources.chat.completions import Completions, AsyncCompletions
|
|
38
|
+
except ImportError:
|
|
39
|
+
logger.debug("OpenAI library not found, skipping instrumentation")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
logger.info("Instrumenting OpenAI...")
|
|
43
|
+
|
|
44
|
+
# We patch the 'create' method of the Completions resource
|
|
45
|
+
# checks if already patched to avoid double patching
|
|
46
|
+
if getattr(Completions.create, "_is_auditi_patched", False):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
original_create = Completions.create
|
|
50
|
+
original_async_create = AsyncCompletions.create
|
|
51
|
+
|
|
52
|
+
@trace_llm(name="OpenAI Chat Completion", standalone=True)
|
|
53
|
+
def patched_create(self, *args, **kwargs):
|
|
54
|
+
# We need to make sure we don't double-trace if the user is already using decorators
|
|
55
|
+
# But for now, simple wrapping using our existing @trace_llm is easiest.
|
|
56
|
+
# It handles standalone vs nested automatically.
|
|
57
|
+
return original_create(self, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
@trace_llm(name="OpenAI Chat Completion", standalone=True)
|
|
60
|
+
async def patched_async_create(self, *args, **kwargs):
|
|
61
|
+
return await original_async_create(self, *args, **kwargs)
|
|
62
|
+
|
|
63
|
+
# Mark as patched
|
|
64
|
+
patched_create._is_auditi_patched = True
|
|
65
|
+
patched_async_create._is_auditi_patched = True
|
|
66
|
+
|
|
67
|
+
# Apply patches
|
|
68
|
+
Completions.create = patched_create
|
|
69
|
+
AsyncCompletions.create = patched_async_create
|
|
70
|
+
|
|
71
|
+
logger.info("OpenAI instrumentation applied successfully")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _instrument_anthropic():
|
|
75
|
+
"""Patch Anthropic client methods"""
|
|
76
|
+
try:
|
|
77
|
+
import anthropic
|
|
78
|
+
from anthropic.resources.messages import Messages, AsyncMessages
|
|
79
|
+
except ImportError:
|
|
80
|
+
logger.debug("Anthropic library not found, skipping instrumentation")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
logger.info("Instrumenting Anthropic...")
|
|
84
|
+
|
|
85
|
+
if getattr(Messages.create, "_is_auditi_patched", False):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
original_create = Messages.create
|
|
89
|
+
original_async_create = AsyncMessages.create
|
|
90
|
+
|
|
91
|
+
@trace_llm(name="Anthropic Message", standalone=True)
|
|
92
|
+
def patched_create(self, *args, **kwargs):
|
|
93
|
+
return original_create(self, *args, **kwargs)
|
|
94
|
+
|
|
95
|
+
@trace_llm(name="Anthropic Message", standalone=True)
|
|
96
|
+
async def patched_async_create(self, *args, **kwargs):
|
|
97
|
+
return await original_async_create(self, *args, **kwargs)
|
|
98
|
+
|
|
99
|
+
patched_create._is_auditi_patched = True
|
|
100
|
+
patched_async_create._is_auditi_patched = True
|
|
101
|
+
|
|
102
|
+
Messages.create = patched_create
|
|
103
|
+
AsyncMessages.create = patched_async_create
|
|
104
|
+
|
|
105
|
+
logger.info("Anthropic instrumentation applied successfully")
|
auditi/types/api_types.py
CHANGED
|
@@ -82,7 +82,18 @@ class TraceInput(BaseModel):
|
|
|
82
82
|
|
|
83
83
|
@field_validator("user_input", mode="before")
|
|
84
84
|
def normalize_user_input(cls, v):
|
|
85
|
-
|
|
85
|
+
if isinstance(v, str):
|
|
86
|
+
return v
|
|
87
|
+
if isinstance(v, list):
|
|
88
|
+
# Extract last user message from chat-format messages
|
|
89
|
+
for msg in reversed(v):
|
|
90
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
91
|
+
content = msg.get("content", "")
|
|
92
|
+
return str(content) if not isinstance(content, str) else content
|
|
93
|
+
return str(v)
|
|
94
|
+
if v is None:
|
|
95
|
+
return ""
|
|
96
|
+
return str(v)
|
|
86
97
|
|
|
87
98
|
|
|
88
99
|
class EvaluationResult(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auditi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Trace, monitor, and evaluate AI agents and LLM applications with simple decorators
|
|
5
5
|
Project-URL: Repository, https://github.com/deduu/auditi
|
|
6
6
|
Project-URL: Issues, https://github.com/deduu/auditi/issues
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
auditi/__init__.py,sha256=
|
|
1
|
+
auditi/__init__.py,sha256=M8iuEdpsUMPiJUDsgJUWANeTo6F3SWIsTzOJHctWrUk,1343
|
|
2
2
|
auditi/client.py,sha256=2prRkKde4Hm1usyGJlA2nUvqmL_-fxhq4-9g5nO98m4,2179
|
|
3
3
|
auditi/context.py,sha256=ooDa1497Nax0EwwydarK4aK34Pi8nlMa1RW_CegVUKU,2050
|
|
4
|
-
auditi/decorators.py,sha256=
|
|
4
|
+
auditi/decorators.py,sha256=SGKRPov7241BNIZ81jbgI4KQKNmbnuFMGjc5VLDmBQE,57874
|
|
5
5
|
auditi/evaluator.py,sha256=TC20LDMlOeaorvfvZI9VJ75JLhvKqIDEXFcToyJmBpk,1291
|
|
6
6
|
auditi/events.py,sha256=w4UAG0AoqrVAmdrx5LIKzOsYB_Hpojb6A-hY_Xv7EGs,6478
|
|
7
|
+
auditi/instrumentation.py,sha256=WV6Tx7nlbT0PAiXo3rUfDoMYtD-U6XArqge1CCkKw2o,3493
|
|
7
8
|
auditi/transport.py,sha256=KT3YSYz2sizG4aWua9H6h49n1WN8ht9Tk4wqlDJj25Y,2448
|
|
8
9
|
auditi/providers/__init__.py,sha256=LRnpvFvvNOv81sXp8MLZnGOFJwZfoqKJxnfGNfEsrdg,1447
|
|
9
10
|
auditi/providers/anthropic.py,sha256=tdfSMVPLDJ4tYC0eqWb9atUpYPDXpTjWkiJRN11XBQM,4868
|
|
@@ -12,8 +13,8 @@ auditi/providers/google.py,sha256=faYZNlP-guERXFLvaBw48ZYIt8mzupwf75p0--_v3dQ,69
|
|
|
12
13
|
auditi/providers/openai.py,sha256=RNVQ--blF5YRD2paBkOylAWSC67BcSOvbOYTJS16RE8,5075
|
|
13
14
|
auditi/providers/registry.py,sha256=hSvhqvMTlFt0JjV7rBu5J-UxsvYZflxPyqddhude5JI,5001
|
|
14
15
|
auditi/types/__init__.py,sha256=8o0vlEn6jTxGgL7dhEGXAMNw1NUX0gVrqyp6YcKGTT4,228
|
|
15
|
-
auditi/types/api_types.py,sha256=
|
|
16
|
-
auditi-0.1.
|
|
17
|
-
auditi-0.1.
|
|
18
|
-
auditi-0.1.
|
|
19
|
-
auditi-0.1.
|
|
16
|
+
auditi/types/api_types.py,sha256=vtZilxfi1U4MU0qnu8XluQnlDtf9kVQ7mDqaD2jOKOA,3213
|
|
17
|
+
auditi-0.1.1.dist-info/METADATA,sha256=O32II9W51eNck_-DSDksa6MsjC7IEmkxhcsmpRewdk4,18911
|
|
18
|
+
auditi-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
auditi-0.1.1.dist-info/licenses/LICENSE,sha256=vYb7Htb7bfnI-Fo1U3rMUQVD-UvJd6MWV_oMXE4WeQE,1084
|
|
20
|
+
auditi-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|