control-zero 0.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.
- control_zero/__init__.py +31 -0
- control_zero/client.py +584 -0
- control_zero/integrations/crewai/__init__.py +53 -0
- control_zero/integrations/crewai/agent.py +267 -0
- control_zero/integrations/crewai/crew.py +381 -0
- control_zero/integrations/crewai/task.py +291 -0
- control_zero/integrations/crewai/tool.py +299 -0
- control_zero/integrations/langchain/__init__.py +58 -0
- control_zero/integrations/langchain/agent.py +311 -0
- control_zero/integrations/langchain/callbacks.py +441 -0
- control_zero/integrations/langchain/chain.py +319 -0
- control_zero/integrations/langchain/graph.py +441 -0
- control_zero/integrations/langchain/tool.py +271 -0
- control_zero/llm/__init__.py +77 -0
- control_zero/llm/anthropic/__init__.py +35 -0
- control_zero/llm/anthropic/client.py +136 -0
- control_zero/llm/anthropic/messages.py +375 -0
- control_zero/llm/base.py +551 -0
- control_zero/llm/cohere/__init__.py +32 -0
- control_zero/llm/cohere/client.py +402 -0
- control_zero/llm/gemini/__init__.py +34 -0
- control_zero/llm/gemini/client.py +486 -0
- control_zero/llm/groq/__init__.py +32 -0
- control_zero/llm/groq/client.py +330 -0
- control_zero/llm/mistral/__init__.py +32 -0
- control_zero/llm/mistral/client.py +319 -0
- control_zero/llm/ollama/__init__.py +31 -0
- control_zero/llm/ollama/client.py +439 -0
- control_zero/llm/openai/__init__.py +34 -0
- control_zero/llm/openai/chat.py +331 -0
- control_zero/llm/openai/client.py +182 -0
- control_zero/logging/__init__.py +5 -0
- control_zero/logging/async_logger.py +65 -0
- control_zero/mcp/__init__.py +5 -0
- control_zero/mcp/middleware.py +148 -0
- control_zero/policy/__init__.py +5 -0
- control_zero/policy/enforcer.py +99 -0
- control_zero/secrets/__init__.py +5 -0
- control_zero/secrets/manager.py +77 -0
- control_zero/types.py +51 -0
- control_zero-0.2.0.dist-info/METADATA +216 -0
- control_zero-0.2.0.dist-info/RECORD +44 -0
- control_zero-0.2.0.dist-info/WHEEL +4 -0
- control_zero-0.2.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Control Zero Callback Handler for LangChain.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive logging and monitoring for all LangChain operations
|
|
5
|
+
including LLM calls, tool executions, chain runs, and agent actions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
14
|
+
from langchain_core.outputs import LLMResult
|
|
15
|
+
from langchain_core.agents import AgentAction, AgentFinish
|
|
16
|
+
from langchain_core.messages import BaseMessage
|
|
17
|
+
LANGCHAIN_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
LANGCHAIN_AVAILABLE = False
|
|
20
|
+
class BaseCallbackHandler:
|
|
21
|
+
pass
|
|
22
|
+
class LLMResult:
|
|
23
|
+
pass
|
|
24
|
+
class AgentAction:
|
|
25
|
+
pass
|
|
26
|
+
class AgentFinish:
|
|
27
|
+
pass
|
|
28
|
+
class BaseMessage:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
from control_zero.client import ControlZeroClient
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ControlZeroCallbackHandler(BaseCallbackHandler):
|
|
35
|
+
"""
|
|
36
|
+
Callback handler that logs all LangChain operations to Control Zero.
|
|
37
|
+
|
|
38
|
+
Captures:
|
|
39
|
+
- LLM calls (start, end, error)
|
|
40
|
+
- Tool executions (start, end, error)
|
|
41
|
+
- Chain runs (start, end, error)
|
|
42
|
+
- Agent actions
|
|
43
|
+
- Token usage and costs
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
from langchain_openai import ChatOpenAI
|
|
47
|
+
from control_zero.integrations.langchain import ControlZeroCallbackHandler
|
|
48
|
+
|
|
49
|
+
client = ControlZeroClient(api_key="...")
|
|
50
|
+
client.initialize()
|
|
51
|
+
|
|
52
|
+
callback = ControlZeroCallbackHandler(client)
|
|
53
|
+
|
|
54
|
+
llm = ChatOpenAI(callbacks=[callback])
|
|
55
|
+
# or
|
|
56
|
+
agent.run("query", callbacks=[callback])
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
client: ControlZeroClient,
|
|
62
|
+
log_prompts: bool = False,
|
|
63
|
+
log_responses: bool = False,
|
|
64
|
+
track_costs: bool = True,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Initialize callback handler.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
client: Control Zero client
|
|
71
|
+
log_prompts: Whether to log prompt content (may contain sensitive data)
|
|
72
|
+
log_responses: Whether to log response content
|
|
73
|
+
track_costs: Whether to track token costs
|
|
74
|
+
"""
|
|
75
|
+
if not LANGCHAIN_AVAILABLE:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"langchain is required. Install with: pip install langchain-core"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
super().__init__()
|
|
81
|
+
self._client = client
|
|
82
|
+
self._log_prompts = log_prompts
|
|
83
|
+
self._log_responses = log_responses
|
|
84
|
+
self._track_costs = track_costs
|
|
85
|
+
|
|
86
|
+
# Tracking state
|
|
87
|
+
self._run_starts: Dict[str, float] = {}
|
|
88
|
+
self._total_tokens = 0
|
|
89
|
+
self._total_cost = 0.0
|
|
90
|
+
|
|
91
|
+
# ==================== LLM Callbacks ====================
|
|
92
|
+
|
|
93
|
+
def on_llm_start(
|
|
94
|
+
self,
|
|
95
|
+
serialized: Dict[str, Any],
|
|
96
|
+
prompts: List[str],
|
|
97
|
+
*,
|
|
98
|
+
run_id: UUID,
|
|
99
|
+
parent_run_id: Optional[UUID] = None,
|
|
100
|
+
tags: Optional[List[str]] = None,
|
|
101
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
102
|
+
**kwargs: Any,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Called when LLM starts."""
|
|
105
|
+
self._run_starts[str(run_id)] = time.perf_counter()
|
|
106
|
+
|
|
107
|
+
model_name = serialized.get("name", "unknown_llm")
|
|
108
|
+
|
|
109
|
+
log_data = {
|
|
110
|
+
"event": "llm_start",
|
|
111
|
+
"model": model_name,
|
|
112
|
+
"num_prompts": len(prompts),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if self._log_prompts:
|
|
116
|
+
log_data["prompts"] = [p[:500] for p in prompts]
|
|
117
|
+
|
|
118
|
+
if tags:
|
|
119
|
+
log_data["tags"] = tags
|
|
120
|
+
|
|
121
|
+
self._client._log(
|
|
122
|
+
tool=f"llm:{model_name}",
|
|
123
|
+
method="start",
|
|
124
|
+
status="running",
|
|
125
|
+
latency_ms=0,
|
|
126
|
+
**log_data
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def on_llm_end(
|
|
130
|
+
self,
|
|
131
|
+
response: LLMResult,
|
|
132
|
+
*,
|
|
133
|
+
run_id: UUID,
|
|
134
|
+
parent_run_id: Optional[UUID] = None,
|
|
135
|
+
**kwargs: Any,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Called when LLM ends."""
|
|
138
|
+
start_time = self._run_starts.pop(str(run_id), time.perf_counter())
|
|
139
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
140
|
+
|
|
141
|
+
# Extract usage info
|
|
142
|
+
llm_output = response.llm_output or {}
|
|
143
|
+
token_usage = llm_output.get("token_usage", {})
|
|
144
|
+
model_name = llm_output.get("model_name", "unknown_llm")
|
|
145
|
+
|
|
146
|
+
prompt_tokens = token_usage.get("prompt_tokens", 0)
|
|
147
|
+
completion_tokens = token_usage.get("completion_tokens", 0)
|
|
148
|
+
total_tokens = token_usage.get("total_tokens", prompt_tokens + completion_tokens)
|
|
149
|
+
|
|
150
|
+
if self._track_costs:
|
|
151
|
+
self._total_tokens += total_tokens
|
|
152
|
+
|
|
153
|
+
log_data = {
|
|
154
|
+
"event": "llm_end",
|
|
155
|
+
"model": model_name,
|
|
156
|
+
"prompt_tokens": prompt_tokens,
|
|
157
|
+
"completion_tokens": completion_tokens,
|
|
158
|
+
"total_tokens": total_tokens,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if self._log_responses and response.generations:
|
|
162
|
+
texts = [g[0].text[:500] if g else "" for g in response.generations]
|
|
163
|
+
log_data["responses"] = texts
|
|
164
|
+
|
|
165
|
+
self._client._log(
|
|
166
|
+
tool=f"llm:{model_name}",
|
|
167
|
+
method="complete",
|
|
168
|
+
status="success",
|
|
169
|
+
latency_ms=latency_ms,
|
|
170
|
+
**log_data
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def on_llm_error(
|
|
174
|
+
self,
|
|
175
|
+
error: BaseException,
|
|
176
|
+
*,
|
|
177
|
+
run_id: UUID,
|
|
178
|
+
parent_run_id: Optional[UUID] = None,
|
|
179
|
+
**kwargs: Any,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Called when LLM errors."""
|
|
182
|
+
start_time = self._run_starts.pop(str(run_id), time.perf_counter())
|
|
183
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
184
|
+
|
|
185
|
+
self._client._log(
|
|
186
|
+
tool="llm:unknown",
|
|
187
|
+
method="complete",
|
|
188
|
+
status="error",
|
|
189
|
+
latency_ms=latency_ms,
|
|
190
|
+
error_type=type(error).__name__,
|
|
191
|
+
error_message=str(error),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# ==================== Chat Model Callbacks ====================
|
|
195
|
+
|
|
196
|
+
def on_chat_model_start(
|
|
197
|
+
self,
|
|
198
|
+
serialized: Dict[str, Any],
|
|
199
|
+
messages: List[List[BaseMessage]],
|
|
200
|
+
*,
|
|
201
|
+
run_id: UUID,
|
|
202
|
+
parent_run_id: Optional[UUID] = None,
|
|
203
|
+
tags: Optional[List[str]] = None,
|
|
204
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
205
|
+
**kwargs: Any,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Called when chat model starts."""
|
|
208
|
+
self._run_starts[str(run_id)] = time.perf_counter()
|
|
209
|
+
|
|
210
|
+
model_name = serialized.get("name", kwargs.get("invocation_params", {}).get("model", "unknown_chat"))
|
|
211
|
+
|
|
212
|
+
log_data = {
|
|
213
|
+
"event": "chat_start",
|
|
214
|
+
"model": model_name,
|
|
215
|
+
"num_messages": sum(len(m) for m in messages),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if tags:
|
|
219
|
+
log_data["tags"] = tags
|
|
220
|
+
|
|
221
|
+
self._client._log(
|
|
222
|
+
tool=f"chat:{model_name}",
|
|
223
|
+
method="start",
|
|
224
|
+
status="running",
|
|
225
|
+
latency_ms=0,
|
|
226
|
+
**log_data
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# ==================== Tool Callbacks ====================
|
|
230
|
+
|
|
231
|
+
def on_tool_start(
|
|
232
|
+
self,
|
|
233
|
+
serialized: Dict[str, Any],
|
|
234
|
+
input_str: str,
|
|
235
|
+
*,
|
|
236
|
+
run_id: UUID,
|
|
237
|
+
parent_run_id: Optional[UUID] = None,
|
|
238
|
+
tags: Optional[List[str]] = None,
|
|
239
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
240
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
241
|
+
**kwargs: Any,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Called when tool starts."""
|
|
244
|
+
self._run_starts[str(run_id)] = time.perf_counter()
|
|
245
|
+
|
|
246
|
+
tool_name = serialized.get("name", "unknown_tool")
|
|
247
|
+
|
|
248
|
+
log_data = {
|
|
249
|
+
"event": "tool_start",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if self._log_prompts:
|
|
253
|
+
log_data["input"] = input_str[:500] if input_str else None
|
|
254
|
+
|
|
255
|
+
self._client._log(
|
|
256
|
+
tool=tool_name,
|
|
257
|
+
method="start",
|
|
258
|
+
status="running",
|
|
259
|
+
latency_ms=0,
|
|
260
|
+
**log_data
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def on_tool_end(
|
|
264
|
+
self,
|
|
265
|
+
output: Any,
|
|
266
|
+
*,
|
|
267
|
+
run_id: UUID,
|
|
268
|
+
parent_run_id: Optional[UUID] = None,
|
|
269
|
+
**kwargs: Any,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Called when tool ends."""
|
|
272
|
+
start_time = self._run_starts.pop(str(run_id), time.perf_counter())
|
|
273
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
274
|
+
|
|
275
|
+
log_data = {"event": "tool_end"}
|
|
276
|
+
|
|
277
|
+
if self._log_responses:
|
|
278
|
+
log_data["output"] = str(output)[:500]
|
|
279
|
+
|
|
280
|
+
self._client._log(
|
|
281
|
+
tool="tool:unknown",
|
|
282
|
+
method="complete",
|
|
283
|
+
status="success",
|
|
284
|
+
latency_ms=latency_ms,
|
|
285
|
+
**log_data
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def on_tool_error(
|
|
289
|
+
self,
|
|
290
|
+
error: BaseException,
|
|
291
|
+
*,
|
|
292
|
+
run_id: UUID,
|
|
293
|
+
parent_run_id: Optional[UUID] = None,
|
|
294
|
+
**kwargs: Any,
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Called when tool errors."""
|
|
297
|
+
start_time = self._run_starts.pop(str(run_id), time.perf_counter())
|
|
298
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
299
|
+
|
|
300
|
+
self._client._log(
|
|
301
|
+
tool="tool:unknown",
|
|
302
|
+
method="complete",
|
|
303
|
+
status="error",
|
|
304
|
+
latency_ms=latency_ms,
|
|
305
|
+
error_type=type(error).__name__,
|
|
306
|
+
error_message=str(error),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# ==================== Chain Callbacks ====================
|
|
310
|
+
|
|
311
|
+
def on_chain_start(
|
|
312
|
+
self,
|
|
313
|
+
serialized: Dict[str, Any],
|
|
314
|
+
inputs: Dict[str, Any],
|
|
315
|
+
*,
|
|
316
|
+
run_id: UUID,
|
|
317
|
+
parent_run_id: Optional[UUID] = None,
|
|
318
|
+
tags: Optional[List[str]] = None,
|
|
319
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
320
|
+
**kwargs: Any,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Called when chain starts."""
|
|
323
|
+
self._run_starts[str(run_id)] = time.perf_counter()
|
|
324
|
+
|
|
325
|
+
chain_name = serialized.get("name", "unknown_chain")
|
|
326
|
+
|
|
327
|
+
log_data = {"event": "chain_start"}
|
|
328
|
+
|
|
329
|
+
if self._log_prompts:
|
|
330
|
+
log_data["inputs"] = {k: str(v)[:200] for k, v in inputs.items()}
|
|
331
|
+
|
|
332
|
+
self._client._log(
|
|
333
|
+
tool=f"chain:{chain_name}",
|
|
334
|
+
method="start",
|
|
335
|
+
status="running",
|
|
336
|
+
latency_ms=0,
|
|
337
|
+
**log_data
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def on_chain_end(
|
|
341
|
+
self,
|
|
342
|
+
outputs: Dict[str, Any],
|
|
343
|
+
*,
|
|
344
|
+
run_id: UUID,
|
|
345
|
+
parent_run_id: Optional[UUID] = None,
|
|
346
|
+
**kwargs: Any,
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Called when chain ends."""
|
|
349
|
+
start_time = self._run_starts.pop(str(run_id), time.perf_counter())
|
|
350
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
351
|
+
|
|
352
|
+
log_data = {"event": "chain_end"}
|
|
353
|
+
|
|
354
|
+
if self._log_responses:
|
|
355
|
+
log_data["outputs"] = {k: str(v)[:200] for k, v in outputs.items()}
|
|
356
|
+
|
|
357
|
+
self._client._log(
|
|
358
|
+
tool="chain:unknown",
|
|
359
|
+
method="complete",
|
|
360
|
+
status="success",
|
|
361
|
+
latency_ms=latency_ms,
|
|
362
|
+
**log_data
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def on_chain_error(
|
|
366
|
+
self,
|
|
367
|
+
error: BaseException,
|
|
368
|
+
*,
|
|
369
|
+
run_id: UUID,
|
|
370
|
+
parent_run_id: Optional[UUID] = None,
|
|
371
|
+
**kwargs: Any,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Called when chain errors."""
|
|
374
|
+
start_time = self._run_starts.pop(str(run_id), time.perf_counter())
|
|
375
|
+
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
|
376
|
+
|
|
377
|
+
self._client._log(
|
|
378
|
+
tool="chain:unknown",
|
|
379
|
+
method="complete",
|
|
380
|
+
status="error",
|
|
381
|
+
latency_ms=latency_ms,
|
|
382
|
+
error_type=type(error).__name__,
|
|
383
|
+
error_message=str(error),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# ==================== Agent Callbacks ====================
|
|
387
|
+
|
|
388
|
+
def on_agent_action(
|
|
389
|
+
self,
|
|
390
|
+
action: AgentAction,
|
|
391
|
+
*,
|
|
392
|
+
run_id: UUID,
|
|
393
|
+
parent_run_id: Optional[UUID] = None,
|
|
394
|
+
**kwargs: Any,
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Called when agent takes an action."""
|
|
397
|
+
self._client._log(
|
|
398
|
+
tool=f"agent:action:{action.tool}",
|
|
399
|
+
method="action",
|
|
400
|
+
status="running",
|
|
401
|
+
latency_ms=0,
|
|
402
|
+
event="agent_action",
|
|
403
|
+
tool_name=action.tool,
|
|
404
|
+
tool_input=str(action.tool_input)[:500] if self._log_prompts else None,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def on_agent_finish(
|
|
408
|
+
self,
|
|
409
|
+
finish: AgentFinish,
|
|
410
|
+
*,
|
|
411
|
+
run_id: UUID,
|
|
412
|
+
parent_run_id: Optional[UUID] = None,
|
|
413
|
+
**kwargs: Any,
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Called when agent finishes."""
|
|
416
|
+
self._client._log(
|
|
417
|
+
tool="agent:finish",
|
|
418
|
+
method="finish",
|
|
419
|
+
status="success",
|
|
420
|
+
latency_ms=0,
|
|
421
|
+
event="agent_finish",
|
|
422
|
+
output=str(finish.return_values)[:500] if self._log_responses else None,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# ==================== Metrics ====================
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def total_tokens(self) -> int:
|
|
429
|
+
"""Get total tokens used."""
|
|
430
|
+
return self._total_tokens
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def total_cost(self) -> float:
|
|
434
|
+
"""Get estimated total cost."""
|
|
435
|
+
return self._total_cost
|
|
436
|
+
|
|
437
|
+
def reset_metrics(self) -> None:
|
|
438
|
+
"""Reset accumulated metrics."""
|
|
439
|
+
self._total_tokens = 0
|
|
440
|
+
self._total_cost = 0.0
|
|
441
|
+
self._run_starts.clear()
|