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.
Files changed (44) hide show
  1. control_zero/__init__.py +31 -0
  2. control_zero/client.py +584 -0
  3. control_zero/integrations/crewai/__init__.py +53 -0
  4. control_zero/integrations/crewai/agent.py +267 -0
  5. control_zero/integrations/crewai/crew.py +381 -0
  6. control_zero/integrations/crewai/task.py +291 -0
  7. control_zero/integrations/crewai/tool.py +299 -0
  8. control_zero/integrations/langchain/__init__.py +58 -0
  9. control_zero/integrations/langchain/agent.py +311 -0
  10. control_zero/integrations/langchain/callbacks.py +441 -0
  11. control_zero/integrations/langchain/chain.py +319 -0
  12. control_zero/integrations/langchain/graph.py +441 -0
  13. control_zero/integrations/langchain/tool.py +271 -0
  14. control_zero/llm/__init__.py +77 -0
  15. control_zero/llm/anthropic/__init__.py +35 -0
  16. control_zero/llm/anthropic/client.py +136 -0
  17. control_zero/llm/anthropic/messages.py +375 -0
  18. control_zero/llm/base.py +551 -0
  19. control_zero/llm/cohere/__init__.py +32 -0
  20. control_zero/llm/cohere/client.py +402 -0
  21. control_zero/llm/gemini/__init__.py +34 -0
  22. control_zero/llm/gemini/client.py +486 -0
  23. control_zero/llm/groq/__init__.py +32 -0
  24. control_zero/llm/groq/client.py +330 -0
  25. control_zero/llm/mistral/__init__.py +32 -0
  26. control_zero/llm/mistral/client.py +319 -0
  27. control_zero/llm/ollama/__init__.py +31 -0
  28. control_zero/llm/ollama/client.py +439 -0
  29. control_zero/llm/openai/__init__.py +34 -0
  30. control_zero/llm/openai/chat.py +331 -0
  31. control_zero/llm/openai/client.py +182 -0
  32. control_zero/logging/__init__.py +5 -0
  33. control_zero/logging/async_logger.py +65 -0
  34. control_zero/mcp/__init__.py +5 -0
  35. control_zero/mcp/middleware.py +148 -0
  36. control_zero/policy/__init__.py +5 -0
  37. control_zero/policy/enforcer.py +99 -0
  38. control_zero/secrets/__init__.py +5 -0
  39. control_zero/secrets/manager.py +77 -0
  40. control_zero/types.py +51 -0
  41. control_zero-0.2.0.dist-info/METADATA +216 -0
  42. control_zero-0.2.0.dist-info/RECORD +44 -0
  43. control_zero-0.2.0.dist-info/WHEEL +4 -0
  44. 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()