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 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
- first_arg = args[0]
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
- user_input = kwargs.get("prompt") or kwargs.get("message") or kwargs.get("query") or ""
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
- first_arg = args[start_idx]
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
- user_input = (
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
- first_arg = args[start_idx]
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
- user_input = (
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
- first_arg = args[start_idx]
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
- user_input = (
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
- return v or ""
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.0
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=UFAo0c3uO1_extt9yazYUS6XGwBeswD-Bb9utjT3zDw,1258
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=ZeQdQDFgDJ-LWG8tLjAnBTWTy7nKOLjH2-WjSmi3GuE,58291
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=Qu57YnfFffrKJwjfGeYib-_Hbd4DKxvf-D1CKmGFE2g,2737
16
- auditi-0.1.0.dist-info/METADATA,sha256=xJIDtvB1s5vZpGScHnlH093OznckDRmB_OYH3MQhW0w,18911
17
- auditi-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
- auditi-0.1.0.dist-info/licenses/LICENSE,sha256=vYb7Htb7bfnI-Fo1U3rMUQVD-UvJd6MWV_oMXE4WeQE,1084
19
- auditi-0.1.0.dist-info/RECORD,,
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