clientcoded 0.1.0__tar.gz
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.
- clientcoded-0.1.0/PKG-INFO +182 -0
- clientcoded-0.1.0/README.md +150 -0
- clientcoded-0.1.0/clientcoded/__init__.py +20 -0
- clientcoded-0.1.0/clientcoded/trace.py +225 -0
- clientcoded-0.1.0/clientcoded.egg-info/PKG-INFO +182 -0
- clientcoded-0.1.0/clientcoded.egg-info/SOURCES.txt +9 -0
- clientcoded-0.1.0/clientcoded.egg-info/dependency_links.txt +1 -0
- clientcoded-0.1.0/clientcoded.egg-info/requires.txt +1 -0
- clientcoded-0.1.0/clientcoded.egg-info/top_level.txt +1 -0
- clientcoded-0.1.0/setup.cfg +4 -0
- clientcoded-0.1.0/setup.py +28 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clientcoded
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ClientCoded SDK - Trace your AI agent's tool calls for root cause analysis
|
|
5
|
+
Home-page: https://github.com/ClientCoded/clientcoded-python
|
|
6
|
+
Author: ClientCoded
|
|
7
|
+
Author-email: travis@clientcoded.com
|
|
8
|
+
Keywords: ai agent testing qa tracing evaluation
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.20.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: keywords
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# ClientCoded Python SDK
|
|
34
|
+
|
|
35
|
+
Trace your AI agent's tool calls for root cause analysis when failures occur.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install clientcoded
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import clientcoded
|
|
47
|
+
|
|
48
|
+
# Configure once at startup
|
|
49
|
+
clientcoded.configure(
|
|
50
|
+
agent_id="your-agent-id",
|
|
51
|
+
api_key="your-api-key"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Decorate any function your agent calls
|
|
55
|
+
@clientcoded.trace
|
|
56
|
+
def get_invoice(invoice_id):
|
|
57
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
58
|
+
|
|
59
|
+
@clientcoded.trace
|
|
60
|
+
def search_knowledge_base(query):
|
|
61
|
+
return pinecone.query(query)
|
|
62
|
+
|
|
63
|
+
@clientcoded.trace
|
|
64
|
+
def update_crm(contact_id, data):
|
|
65
|
+
return hubspot.update_contact(contact_id, data)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Conversation Tracking
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# At the start of each conversation
|
|
72
|
+
clientcoded.set_conversation_id("conv-123")
|
|
73
|
+
|
|
74
|
+
# At each turn
|
|
75
|
+
clientcoded.set_turn(1)
|
|
76
|
+
response = agent.handle_message("What is my invoice total?")
|
|
77
|
+
|
|
78
|
+
clientcoded.set_turn(2)
|
|
79
|
+
response = agent.handle_message("Can you break that down by line item?")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Async Support
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
@clientcoded.trace_async
|
|
86
|
+
async def get_invoice(invoice_id):
|
|
87
|
+
return await stripe.Invoice.aretrieve(invoice_id)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Custom Names
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
@clientcoded.trace(name="stripe_invoice_lookup")
|
|
94
|
+
def get_invoice(invoice_id):
|
|
95
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Manual Logging
|
|
99
|
+
|
|
100
|
+
For complex operations where decorators don't fit:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import time
|
|
104
|
+
|
|
105
|
+
start = time.time()
|
|
106
|
+
try:
|
|
107
|
+
result = complex_multi_step_operation()
|
|
108
|
+
clientcoded.log_trace(
|
|
109
|
+
"complex_operation",
|
|
110
|
+
input_data={"step": "final"},
|
|
111
|
+
output_data=result,
|
|
112
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
clientcoded.log_trace(
|
|
116
|
+
"complex_operation",
|
|
117
|
+
input_data={"step": "final"},
|
|
118
|
+
error=str(e),
|
|
119
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
120
|
+
)
|
|
121
|
+
raise
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## LangChain Integration
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from langchain.tools import tool
|
|
128
|
+
import clientcoded
|
|
129
|
+
|
|
130
|
+
clientcoded.configure(agent_id="your-agent-id", api_key="your-api-key")
|
|
131
|
+
|
|
132
|
+
@tool
|
|
133
|
+
@clientcoded.trace
|
|
134
|
+
def search_database(query: str) -> str:
|
|
135
|
+
"""Search the company database."""
|
|
136
|
+
return db.execute(query)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## LlamaIndex Integration
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from llama_index.core.tools import FunctionTool
|
|
143
|
+
import clientcoded
|
|
144
|
+
|
|
145
|
+
clientcoded.configure(agent_id="your-agent-id", api_key="your-api-key")
|
|
146
|
+
|
|
147
|
+
@clientcoded.trace
|
|
148
|
+
def query_index(question: str) -> str:
|
|
149
|
+
return index.query(question)
|
|
150
|
+
|
|
151
|
+
tool = FunctionTool.from_defaults(fn=query_index)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## What Gets Traced
|
|
155
|
+
|
|
156
|
+
For each decorated function call, the SDK logs:
|
|
157
|
+
- Function name (or custom name)
|
|
158
|
+
- Input arguments (truncated to 2000 chars)
|
|
159
|
+
- Output (truncated to 2000 chars)
|
|
160
|
+
- Error message if the function threw
|
|
161
|
+
- Latency in milliseconds
|
|
162
|
+
- Conversation ID and turn number (if set)
|
|
163
|
+
|
|
164
|
+
## What Doesn't Get Traced
|
|
165
|
+
|
|
166
|
+
- The SDK never captures environment variables
|
|
167
|
+
- The SDK never captures file contents
|
|
168
|
+
- Large inputs/outputs are truncated, not stored in full
|
|
169
|
+
- All trace sends are fire-and-forget with a 2-second timeout
|
|
170
|
+
|
|
171
|
+
## Safety
|
|
172
|
+
|
|
173
|
+
- The SDK will **never** break your agent. Every trace send is wrapped in try/catch.
|
|
174
|
+
- Trace sends happen in background threads. Zero impact on response latency.
|
|
175
|
+
- If the ClientCoded API is down, traces are silently dropped. Your agent continues normally.
|
|
176
|
+
- No sensitive data filtering in v0.1. Do not decorate functions that handle passwords, tokens, or PII directly. Wrap them in a function that sanitizes first.
|
|
177
|
+
|
|
178
|
+
## Configuration
|
|
179
|
+
|
|
180
|
+
| Environment Variable | Default | Description |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| CLIENTCODED_TRACE_URL | https://clientcoded.app.n8n.cloud/webhook/ap-35-trace-ingest | Custom trace endpoint |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# ClientCoded Python SDK
|
|
2
|
+
|
|
3
|
+
Trace your AI agent's tool calls for root cause analysis when failures occur.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install clientcoded
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import clientcoded
|
|
15
|
+
|
|
16
|
+
# Configure once at startup
|
|
17
|
+
clientcoded.configure(
|
|
18
|
+
agent_id="your-agent-id",
|
|
19
|
+
api_key="your-api-key"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Decorate any function your agent calls
|
|
23
|
+
@clientcoded.trace
|
|
24
|
+
def get_invoice(invoice_id):
|
|
25
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
26
|
+
|
|
27
|
+
@clientcoded.trace
|
|
28
|
+
def search_knowledge_base(query):
|
|
29
|
+
return pinecone.query(query)
|
|
30
|
+
|
|
31
|
+
@clientcoded.trace
|
|
32
|
+
def update_crm(contact_id, data):
|
|
33
|
+
return hubspot.update_contact(contact_id, data)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Conversation Tracking
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# At the start of each conversation
|
|
40
|
+
clientcoded.set_conversation_id("conv-123")
|
|
41
|
+
|
|
42
|
+
# At each turn
|
|
43
|
+
clientcoded.set_turn(1)
|
|
44
|
+
response = agent.handle_message("What is my invoice total?")
|
|
45
|
+
|
|
46
|
+
clientcoded.set_turn(2)
|
|
47
|
+
response = agent.handle_message("Can you break that down by line item?")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Async Support
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
@clientcoded.trace_async
|
|
54
|
+
async def get_invoice(invoice_id):
|
|
55
|
+
return await stripe.Invoice.aretrieve(invoice_id)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Custom Names
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
@clientcoded.trace(name="stripe_invoice_lookup")
|
|
62
|
+
def get_invoice(invoice_id):
|
|
63
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Manual Logging
|
|
67
|
+
|
|
68
|
+
For complex operations where decorators don't fit:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import time
|
|
72
|
+
|
|
73
|
+
start = time.time()
|
|
74
|
+
try:
|
|
75
|
+
result = complex_multi_step_operation()
|
|
76
|
+
clientcoded.log_trace(
|
|
77
|
+
"complex_operation",
|
|
78
|
+
input_data={"step": "final"},
|
|
79
|
+
output_data=result,
|
|
80
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
81
|
+
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
clientcoded.log_trace(
|
|
84
|
+
"complex_operation",
|
|
85
|
+
input_data={"step": "final"},
|
|
86
|
+
error=str(e),
|
|
87
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
88
|
+
)
|
|
89
|
+
raise
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## LangChain Integration
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from langchain.tools import tool
|
|
96
|
+
import clientcoded
|
|
97
|
+
|
|
98
|
+
clientcoded.configure(agent_id="your-agent-id", api_key="your-api-key")
|
|
99
|
+
|
|
100
|
+
@tool
|
|
101
|
+
@clientcoded.trace
|
|
102
|
+
def search_database(query: str) -> str:
|
|
103
|
+
"""Search the company database."""
|
|
104
|
+
return db.execute(query)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## LlamaIndex Integration
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from llama_index.core.tools import FunctionTool
|
|
111
|
+
import clientcoded
|
|
112
|
+
|
|
113
|
+
clientcoded.configure(agent_id="your-agent-id", api_key="your-api-key")
|
|
114
|
+
|
|
115
|
+
@clientcoded.trace
|
|
116
|
+
def query_index(question: str) -> str:
|
|
117
|
+
return index.query(question)
|
|
118
|
+
|
|
119
|
+
tool = FunctionTool.from_defaults(fn=query_index)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## What Gets Traced
|
|
123
|
+
|
|
124
|
+
For each decorated function call, the SDK logs:
|
|
125
|
+
- Function name (or custom name)
|
|
126
|
+
- Input arguments (truncated to 2000 chars)
|
|
127
|
+
- Output (truncated to 2000 chars)
|
|
128
|
+
- Error message if the function threw
|
|
129
|
+
- Latency in milliseconds
|
|
130
|
+
- Conversation ID and turn number (if set)
|
|
131
|
+
|
|
132
|
+
## What Doesn't Get Traced
|
|
133
|
+
|
|
134
|
+
- The SDK never captures environment variables
|
|
135
|
+
- The SDK never captures file contents
|
|
136
|
+
- Large inputs/outputs are truncated, not stored in full
|
|
137
|
+
- All trace sends are fire-and-forget with a 2-second timeout
|
|
138
|
+
|
|
139
|
+
## Safety
|
|
140
|
+
|
|
141
|
+
- The SDK will **never** break your agent. Every trace send is wrapped in try/catch.
|
|
142
|
+
- Trace sends happen in background threads. Zero impact on response latency.
|
|
143
|
+
- If the ClientCoded API is down, traces are silently dropped. Your agent continues normally.
|
|
144
|
+
- No sensitive data filtering in v0.1. Do not decorate functions that handle passwords, tokens, or PII directly. Wrap them in a function that sanitizes first.
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
| Environment Variable | Default | Description |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| CLIENTCODED_TRACE_URL | https://clientcoded.app.n8n.cloud/webhook/ap-35-trace-ingest | Custom trace endpoint |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .trace import (
|
|
2
|
+
configure,
|
|
3
|
+
set_conversation_id,
|
|
4
|
+
set_turn,
|
|
5
|
+
increment_turn,
|
|
6
|
+
trace,
|
|
7
|
+
trace_async,
|
|
8
|
+
log_trace,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = [
|
|
13
|
+
"configure",
|
|
14
|
+
"set_conversation_id",
|
|
15
|
+
"set_turn",
|
|
16
|
+
"increment_turn",
|
|
17
|
+
"trace",
|
|
18
|
+
"trace_async",
|
|
19
|
+
"log_trace",
|
|
20
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# ClientCoded Python SDK
|
|
2
|
+
# pip install clientcoded
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
import functools
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
import threading
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
CLIENTCODED_API = os.environ.get(
|
|
12
|
+
"CLIENTCODED_TRACE_URL",
|
|
13
|
+
"https://clientcoded.app.n8n.cloud/webhook/ap-35-trace-ingest"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_context = threading.local()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def configure(agent_id, api_key, trace_url=None):
|
|
20
|
+
"""
|
|
21
|
+
Configure the SDK globally.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
agent_id: Your agent's ID from ClientCoded dashboard
|
|
25
|
+
api_key: Your API key from ClientCoded dashboard
|
|
26
|
+
trace_url: Optional custom trace endpoint URL
|
|
27
|
+
"""
|
|
28
|
+
_context.agent_id = agent_id
|
|
29
|
+
_context.api_key = api_key
|
|
30
|
+
if trace_url:
|
|
31
|
+
global CLIENTCODED_API
|
|
32
|
+
CLIENTCODED_API = trace_url
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_conversation_id(conversation_id):
|
|
36
|
+
"""Call this at the start of each conversation."""
|
|
37
|
+
_context.conversation_id = conversation_id
|
|
38
|
+
_context.turn = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def set_turn(turn_number):
|
|
42
|
+
"""Call this at the start of each turn."""
|
|
43
|
+
_context.turn = turn_number
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def increment_turn():
|
|
47
|
+
"""Increment the turn counter by 1."""
|
|
48
|
+
current = getattr(_context, 'turn', 0)
|
|
49
|
+
_context.turn = current + 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def trace(func=None, *, name=None):
|
|
53
|
+
"""
|
|
54
|
+
Decorator that traces a function call.
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
@trace
|
|
58
|
+
def get_invoice(invoice_id):
|
|
59
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
60
|
+
|
|
61
|
+
@trace(name="stripe_lookup")
|
|
62
|
+
def get_invoice(invoice_id):
|
|
63
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
64
|
+
"""
|
|
65
|
+
def decorator(fn):
|
|
66
|
+
@functools.wraps(fn)
|
|
67
|
+
def wrapper(*args, **kwargs):
|
|
68
|
+
trace_id = str(uuid.uuid4())
|
|
69
|
+
tool_name = name or fn.__name__
|
|
70
|
+
start = time.time()
|
|
71
|
+
error = None
|
|
72
|
+
result = None
|
|
73
|
+
try:
|
|
74
|
+
result = fn(*args, **kwargs)
|
|
75
|
+
return result
|
|
76
|
+
except Exception as e:
|
|
77
|
+
error = str(e)
|
|
78
|
+
raise
|
|
79
|
+
finally:
|
|
80
|
+
_send_trace(
|
|
81
|
+
trace_id=trace_id,
|
|
82
|
+
tool_name=tool_name,
|
|
83
|
+
args=args,
|
|
84
|
+
kwargs=kwargs,
|
|
85
|
+
result=result,
|
|
86
|
+
error=error,
|
|
87
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
88
|
+
)
|
|
89
|
+
return wrapper
|
|
90
|
+
|
|
91
|
+
if func is not None:
|
|
92
|
+
return decorator(func)
|
|
93
|
+
return decorator
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def trace_async(func=None, *, name=None):
|
|
97
|
+
"""
|
|
98
|
+
Decorator that traces an async function call.
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
@trace_async
|
|
102
|
+
async def get_invoice(invoice_id):
|
|
103
|
+
return await stripe.Invoice.aretrieve(invoice_id)
|
|
104
|
+
"""
|
|
105
|
+
def decorator(fn):
|
|
106
|
+
@functools.wraps(fn)
|
|
107
|
+
async def wrapper(*args, **kwargs):
|
|
108
|
+
trace_id = str(uuid.uuid4())
|
|
109
|
+
tool_name = name or fn.__name__
|
|
110
|
+
start = time.time()
|
|
111
|
+
error = None
|
|
112
|
+
result = None
|
|
113
|
+
try:
|
|
114
|
+
result = await fn(*args, **kwargs)
|
|
115
|
+
return result
|
|
116
|
+
except Exception as e:
|
|
117
|
+
error = str(e)
|
|
118
|
+
raise
|
|
119
|
+
finally:
|
|
120
|
+
_send_trace(
|
|
121
|
+
trace_id=trace_id,
|
|
122
|
+
tool_name=tool_name,
|
|
123
|
+
args=args,
|
|
124
|
+
kwargs=kwargs,
|
|
125
|
+
result=result,
|
|
126
|
+
error=error,
|
|
127
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
128
|
+
)
|
|
129
|
+
return wrapper
|
|
130
|
+
|
|
131
|
+
if func is not None:
|
|
132
|
+
return decorator(func)
|
|
133
|
+
return decorator
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def log_trace(tool_name, input_data=None, output_data=None, error=None, latency_ms=None):
|
|
137
|
+
"""
|
|
138
|
+
Manual trace logging for cases where decorators don't fit.
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
start = time.time()
|
|
142
|
+
try:
|
|
143
|
+
result = some_complex_operation()
|
|
144
|
+
log_trace("complex_op", input_data={"key": "val"}, output_data=result)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
log_trace("complex_op", input_data={"key": "val"}, error=str(e))
|
|
147
|
+
"""
|
|
148
|
+
_send_trace(
|
|
149
|
+
trace_id=str(uuid.uuid4()),
|
|
150
|
+
tool_name=tool_name,
|
|
151
|
+
args=(),
|
|
152
|
+
kwargs=input_data or {},
|
|
153
|
+
result=output_data,
|
|
154
|
+
error=error,
|
|
155
|
+
latency_ms=latency_ms
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _send_trace(trace_id, tool_name, args, kwargs, result, error, latency_ms):
|
|
160
|
+
"""Fire-and-forget trace send. Never breaks the customer's agent."""
|
|
161
|
+
agent_id = getattr(_context, 'agent_id', None)
|
|
162
|
+
api_key = getattr(_context, 'api_key', None)
|
|
163
|
+
|
|
164
|
+
if not agent_id or not api_key:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
payload = {
|
|
168
|
+
"agent_id": agent_id,
|
|
169
|
+
"api_key": api_key,
|
|
170
|
+
"trace_id": trace_id,
|
|
171
|
+
"conversation_id": getattr(_context, 'conversation_id', None),
|
|
172
|
+
"turn": getattr(_context, 'turn', None),
|
|
173
|
+
"tool_name": tool_name,
|
|
174
|
+
"input": _safe_serialize_input(args, kwargs),
|
|
175
|
+
"output": _safe_serialize_output(result),
|
|
176
|
+
"error": error,
|
|
177
|
+
"latency_ms": latency_ms,
|
|
178
|
+
"timestamp": time.time()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Fire and forget in a background thread
|
|
182
|
+
thread = threading.Thread(target=_post_trace, args=(payload,), daemon=True)
|
|
183
|
+
thread.start()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _post_trace(payload):
|
|
187
|
+
"""Send trace to ClientCoded. Silent failure."""
|
|
188
|
+
try:
|
|
189
|
+
requests.post(
|
|
190
|
+
CLIENTCODED_API,
|
|
191
|
+
json=payload,
|
|
192
|
+
timeout=2
|
|
193
|
+
)
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _safe_serialize_input(args, kwargs):
|
|
199
|
+
"""Serialize inputs, truncate large values."""
|
|
200
|
+
try:
|
|
201
|
+
serialized = {}
|
|
202
|
+
if args:
|
|
203
|
+
serialized["args"] = [_truncate(str(a)) for a in args]
|
|
204
|
+
if kwargs:
|
|
205
|
+
serialized["kwargs"] = {k: _truncate(str(v)) for k, v in kwargs.items()}
|
|
206
|
+
return serialized
|
|
207
|
+
except Exception:
|
|
208
|
+
return {"error": "unserializable"}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _safe_serialize_output(result):
|
|
212
|
+
"""Serialize output, truncate large responses."""
|
|
213
|
+
if result is None:
|
|
214
|
+
return None
|
|
215
|
+
try:
|
|
216
|
+
return _truncate(str(result))
|
|
217
|
+
except Exception:
|
|
218
|
+
return "unserializable"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _truncate(s, max_len=2000):
|
|
222
|
+
"""Truncate string to max length."""
|
|
223
|
+
if len(s) > max_len:
|
|
224
|
+
return s[:max_len] + "... (truncated)"
|
|
225
|
+
return s
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clientcoded
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ClientCoded SDK - Trace your AI agent's tool calls for root cause analysis
|
|
5
|
+
Home-page: https://github.com/ClientCoded/clientcoded-python
|
|
6
|
+
Author: ClientCoded
|
|
7
|
+
Author-email: travis@clientcoded.com
|
|
8
|
+
Keywords: ai agent testing qa tracing evaluation
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.20.0
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: keywords
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# ClientCoded Python SDK
|
|
34
|
+
|
|
35
|
+
Trace your AI agent's tool calls for root cause analysis when failures occur.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install clientcoded
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import clientcoded
|
|
47
|
+
|
|
48
|
+
# Configure once at startup
|
|
49
|
+
clientcoded.configure(
|
|
50
|
+
agent_id="your-agent-id",
|
|
51
|
+
api_key="your-api-key"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Decorate any function your agent calls
|
|
55
|
+
@clientcoded.trace
|
|
56
|
+
def get_invoice(invoice_id):
|
|
57
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
58
|
+
|
|
59
|
+
@clientcoded.trace
|
|
60
|
+
def search_knowledge_base(query):
|
|
61
|
+
return pinecone.query(query)
|
|
62
|
+
|
|
63
|
+
@clientcoded.trace
|
|
64
|
+
def update_crm(contact_id, data):
|
|
65
|
+
return hubspot.update_contact(contact_id, data)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Conversation Tracking
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# At the start of each conversation
|
|
72
|
+
clientcoded.set_conversation_id("conv-123")
|
|
73
|
+
|
|
74
|
+
# At each turn
|
|
75
|
+
clientcoded.set_turn(1)
|
|
76
|
+
response = agent.handle_message("What is my invoice total?")
|
|
77
|
+
|
|
78
|
+
clientcoded.set_turn(2)
|
|
79
|
+
response = agent.handle_message("Can you break that down by line item?")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Async Support
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
@clientcoded.trace_async
|
|
86
|
+
async def get_invoice(invoice_id):
|
|
87
|
+
return await stripe.Invoice.aretrieve(invoice_id)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Custom Names
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
@clientcoded.trace(name="stripe_invoice_lookup")
|
|
94
|
+
def get_invoice(invoice_id):
|
|
95
|
+
return stripe.Invoice.retrieve(invoice_id)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Manual Logging
|
|
99
|
+
|
|
100
|
+
For complex operations where decorators don't fit:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import time
|
|
104
|
+
|
|
105
|
+
start = time.time()
|
|
106
|
+
try:
|
|
107
|
+
result = complex_multi_step_operation()
|
|
108
|
+
clientcoded.log_trace(
|
|
109
|
+
"complex_operation",
|
|
110
|
+
input_data={"step": "final"},
|
|
111
|
+
output_data=result,
|
|
112
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
clientcoded.log_trace(
|
|
116
|
+
"complex_operation",
|
|
117
|
+
input_data={"step": "final"},
|
|
118
|
+
error=str(e),
|
|
119
|
+
latency_ms=int((time.time() - start) * 1000)
|
|
120
|
+
)
|
|
121
|
+
raise
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## LangChain Integration
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from langchain.tools import tool
|
|
128
|
+
import clientcoded
|
|
129
|
+
|
|
130
|
+
clientcoded.configure(agent_id="your-agent-id", api_key="your-api-key")
|
|
131
|
+
|
|
132
|
+
@tool
|
|
133
|
+
@clientcoded.trace
|
|
134
|
+
def search_database(query: str) -> str:
|
|
135
|
+
"""Search the company database."""
|
|
136
|
+
return db.execute(query)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## LlamaIndex Integration
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from llama_index.core.tools import FunctionTool
|
|
143
|
+
import clientcoded
|
|
144
|
+
|
|
145
|
+
clientcoded.configure(agent_id="your-agent-id", api_key="your-api-key")
|
|
146
|
+
|
|
147
|
+
@clientcoded.trace
|
|
148
|
+
def query_index(question: str) -> str:
|
|
149
|
+
return index.query(question)
|
|
150
|
+
|
|
151
|
+
tool = FunctionTool.from_defaults(fn=query_index)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## What Gets Traced
|
|
155
|
+
|
|
156
|
+
For each decorated function call, the SDK logs:
|
|
157
|
+
- Function name (or custom name)
|
|
158
|
+
- Input arguments (truncated to 2000 chars)
|
|
159
|
+
- Output (truncated to 2000 chars)
|
|
160
|
+
- Error message if the function threw
|
|
161
|
+
- Latency in milliseconds
|
|
162
|
+
- Conversation ID and turn number (if set)
|
|
163
|
+
|
|
164
|
+
## What Doesn't Get Traced
|
|
165
|
+
|
|
166
|
+
- The SDK never captures environment variables
|
|
167
|
+
- The SDK never captures file contents
|
|
168
|
+
- Large inputs/outputs are truncated, not stored in full
|
|
169
|
+
- All trace sends are fire-and-forget with a 2-second timeout
|
|
170
|
+
|
|
171
|
+
## Safety
|
|
172
|
+
|
|
173
|
+
- The SDK will **never** break your agent. Every trace send is wrapped in try/catch.
|
|
174
|
+
- Trace sends happen in background threads. Zero impact on response latency.
|
|
175
|
+
- If the ClientCoded API is down, traces are silently dropped. Your agent continues normally.
|
|
176
|
+
- No sensitive data filtering in v0.1. Do not decorate functions that handle passwords, tokens, or PII directly. Wrap them in a function that sanitizes first.
|
|
177
|
+
|
|
178
|
+
## Configuration
|
|
179
|
+
|
|
180
|
+
| Environment Variable | Default | Description |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| CLIENTCODED_TRACE_URL | https://clientcoded.app.n8n.cloud/webhook/ap-35-trace-ingest | Custom trace endpoint |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.20.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clientcoded
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="clientcoded",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="ClientCoded SDK - Trace your AI agent's tool calls for root cause analysis",
|
|
7
|
+
long_description=open("README.md").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="ClientCoded",
|
|
10
|
+
author_email="travis@clientcoded.com",
|
|
11
|
+
url="https://github.com/ClientCoded/clientcoded-python",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=["requests>=2.20.0"],
|
|
14
|
+
python_requires=">=3.8",
|
|
15
|
+
classifiers=[
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
],
|
|
27
|
+
keywords="ai agent testing qa tracing evaluation",
|
|
28
|
+
)
|