agentwatch-dev 0.1.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.
agentwatch/__init__.py ADDED
@@ -0,0 +1,74 @@
1
+ """AgentWatch — predict when production AI agents degrade before they visibly fail.
2
+
3
+ Quick start:
4
+
5
+ import agentwatch
6
+ agentwatch.configure(api_key="aw_live_...", agent_id="<agent-uuid>")
7
+
8
+ # Option A — auto-instrument any OpenAI-compatible client (Groq / Cerebras / OpenAI):
9
+ from openai import OpenAI
10
+ client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=GROQ_KEY)
11
+ agentwatch.instrument_openai(client) # every chat completion now emits a trace
12
+
13
+ # Option B — wrap your agent function:
14
+ @agentwatch.watch()
15
+ def run_agent(query: str) -> str:
16
+ ...
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any, Callable, Dict, Optional
22
+
23
+ from .client import AgentWatchClient
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = [
27
+ "AgentWatchClient",
28
+ "configure",
29
+ "watch",
30
+ "instrument_openai",
31
+ "trace",
32
+ "__version__",
33
+ ]
34
+
35
+ _default_client: Optional[AgentWatchClient] = None
36
+
37
+
38
+ def configure(api_key: str, agent_id: str, **kwargs: Any) -> AgentWatchClient:
39
+ """Set the process-wide default client used by the module-level helpers."""
40
+ global _default_client
41
+ _default_client = AgentWatchClient(api_key=api_key, agent_id=agent_id, **kwargs)
42
+ return _default_client
43
+
44
+
45
+ def _require_default() -> AgentWatchClient:
46
+ if _default_client is None:
47
+ raise RuntimeError(
48
+ "AgentWatch is not configured. Call "
49
+ "agentwatch.configure(api_key=..., agent_id=...) first, "
50
+ "or pass client=AgentWatchClient(...) explicitly."
51
+ )
52
+ return _default_client
53
+
54
+
55
+ def watch(
56
+ fn: Optional[Callable] = None,
57
+ *,
58
+ client: Optional[AgentWatchClient] = None,
59
+ metadata: Optional[Dict[str, Any]] = None,
60
+ ) -> Callable:
61
+ """Wrap a sync/async agent function. Use `@agentwatch.watch()` or `watch(fn)`."""
62
+ return (client or _require_default()).watch(fn, metadata=metadata)
63
+
64
+
65
+ def instrument_openai(
66
+ openai_client: Any, *, client: Optional[AgentWatchClient] = None
67
+ ) -> Any:
68
+ """Auto-instrument an OpenAI-compatible client (OpenAI / Groq / Cerebras / …)."""
69
+ return (client or _require_default()).instrument_openai(openai_client)
70
+
71
+
72
+ def trace(*, client: Optional[AgentWatchClient] = None, **kwargs: Any) -> None:
73
+ """Manually submit a trace via the default (or given) client."""
74
+ (client or _require_default()).trace(**kwargs)
agentwatch/_extract.py ADDED
@@ -0,0 +1,105 @@
1
+ """Helpers to pull trace fields out of LLM responses.
2
+
3
+ Handles both the official `openai` client's pydantic objects (attribute access)
4
+ and plain dicts (key access), so the same code covers OpenAI, Groq, Cerebras,
5
+ Together, vLLM, and anyone returning an OpenAI-compatible shape.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ def _get(obj: Any, key: str) -> Any:
15
+ """Read `key` from a dict (by key) or an object (by attribute)."""
16
+ if obj is None:
17
+ return None
18
+ if isinstance(obj, dict):
19
+ return obj.get(key)
20
+ return getattr(obj, key, None)
21
+
22
+
23
+ def truncate(value: str, max_length: int) -> str:
24
+ if len(value) <= max_length:
25
+ return value
26
+ return value[:max_length] + "…[truncated]"
27
+
28
+
29
+ def safe_serialize(value: Any, max_length: int) -> str:
30
+ if value is None:
31
+ return ""
32
+ if isinstance(value, str):
33
+ return truncate(value, max_length)
34
+ try:
35
+ return truncate(json.dumps(value, default=str), max_length)
36
+ except Exception:
37
+ return truncate(str(value), max_length)
38
+
39
+
40
+ def extract_output(result: Any, max_length: int) -> str:
41
+ """Prefer the assistant message text; fall back to a serialized dump."""
42
+ choices = _get(result, "choices")
43
+ if choices:
44
+ try:
45
+ message = _get(choices[0], "message")
46
+ content = _get(message, "content")
47
+ if isinstance(content, str):
48
+ return truncate(content, max_length)
49
+ except Exception:
50
+ pass
51
+ if isinstance(result, str):
52
+ return truncate(result, max_length)
53
+ return safe_serialize(result, max_length)
54
+
55
+
56
+ def extract_model(result: Any) -> str:
57
+ model = _get(result, "model")
58
+ return model if isinstance(model, str) and model else "unknown"
59
+
60
+
61
+ def extract_tokens(result: Any) -> Dict[str, int]:
62
+ usage = _get(result, "usage")
63
+ if usage is None:
64
+ return {}
65
+ out: Dict[str, int] = {}
66
+ prompt = _get(usage, "prompt_tokens")
67
+ completion = _get(usage, "completion_tokens")
68
+ total = _get(usage, "total_tokens")
69
+ if isinstance(prompt, int):
70
+ out["promptTokens"] = prompt
71
+ if isinstance(completion, int):
72
+ out["completionTokens"] = completion
73
+ if isinstance(total, int):
74
+ out["totalTokens"] = total
75
+ return out
76
+
77
+
78
+ def extract_tools(result: Any) -> List[str]:
79
+ choices = _get(result, "choices")
80
+ if not choices:
81
+ return []
82
+ message = _get(choices[0], "message")
83
+ tool_calls = _get(message, "tool_calls")
84
+ if not tool_calls:
85
+ return []
86
+ names: List[str] = []
87
+ for call in tool_calls:
88
+ fn = _get(call, "function")
89
+ name = _get(fn, "name")
90
+ if isinstance(name, str):
91
+ names.append(name)
92
+ return names
93
+
94
+
95
+ def input_from_messages(messages: Any, max_length: int) -> str:
96
+ """Represent a chat request by its last user message (most descriptive)."""
97
+ try:
98
+ for msg in reversed(list(messages)):
99
+ if _get(msg, "role") == "user":
100
+ content = _get(msg, "content")
101
+ if isinstance(content, str):
102
+ return truncate(content, max_length)
103
+ except Exception:
104
+ pass
105
+ return safe_serialize(messages, max_length)
@@ -0,0 +1,38 @@
1
+ """Fire-and-forget trace delivery.
2
+
3
+ Monitoring must never surface an error to the production caller and must never
4
+ add latency to its request path, so every trace is POSTed on a daemon thread and
5
+ all exceptions are swallowed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import threading
12
+ import urllib.request
13
+ from typing import Any, Dict
14
+
15
+ SDK_VERSION = "0.1.0"
16
+
17
+
18
+ def post_trace(endpoint: str, api_key: str, payload: Dict[str, Any], timeout: float) -> None:
19
+ def _send() -> None:
20
+ try:
21
+ data = json.dumps(payload).encode("utf-8")
22
+ request = urllib.request.Request(
23
+ f"{endpoint}/v1/trace",
24
+ data=data,
25
+ method="POST",
26
+ headers={
27
+ "Authorization": f"Bearer {api_key}",
28
+ "Content-Type": "application/json",
29
+ "X-SDK-Version": SDK_VERSION,
30
+ "User-Agent": f"agentwatch-python/{SDK_VERSION}",
31
+ },
32
+ )
33
+ urllib.request.urlopen(request, timeout=timeout).close()
34
+ except Exception:
35
+ # Intentional no-op: telemetry failures must not affect the caller.
36
+ pass
37
+
38
+ threading.Thread(target=_send, daemon=True).start()
agentwatch/client.py ADDED
@@ -0,0 +1,283 @@
1
+ """AgentWatchClient — the stateful entry point.
2
+
3
+ Mirrors the TypeScript SDK's clean-client design: it only captures a trace and
4
+ POSTs it to the ingestion API. All drift math (CUSUM/EWMA/judge/scoring) runs
5
+ server-side, so this package ships none of the engine.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import functools
12
+ import time
13
+ import uuid
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Callable, Dict, List, Optional
16
+
17
+ from . import _extract
18
+ from ._transport import post_trace
19
+
20
+ DEFAULT_ENDPOINT = "https://api.agentwatch.dev"
21
+ DEFAULT_MAX_PREVIEW = 5000
22
+ DEFAULT_TIMEOUT = 5.0 # seconds
23
+ MAX_LATENCY_MS = 600_000
24
+
25
+
26
+ def _now_iso() -> str:
27
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
28
+
29
+
30
+ def _elapsed_ms(start: float) -> float:
31
+ return (time.perf_counter() - start) * 1000.0
32
+
33
+
34
+ class AgentWatchClient:
35
+ """Reusable client for a single agent.
36
+
37
+ Example:
38
+ client = AgentWatchClient(api_key="aw_live_...", agent_id="<uuid>")
39
+ run = client.watch(run_agent) # wrap a function
40
+ client.instrument_openai(openai_client) # or auto-instrument a client
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: str,
46
+ agent_id: str,
47
+ endpoint: Optional[str] = None,
48
+ max_preview_length: int = DEFAULT_MAX_PREVIEW,
49
+ timeout: float = DEFAULT_TIMEOUT,
50
+ metadata: Optional[Dict[str, Any]] = None,
51
+ ) -> None:
52
+ if not api_key:
53
+ raise ValueError("AgentWatch: api_key is required")
54
+ if not agent_id:
55
+ raise ValueError("AgentWatch: agent_id is required")
56
+ self.api_key = api_key
57
+ self.agent_id = agent_id
58
+ self.endpoint = (endpoint or DEFAULT_ENDPOINT).rstrip("/")
59
+ self.max_preview_length = max_preview_length
60
+ self.timeout = timeout
61
+ self.metadata: Dict[str, Any] = dict(metadata or {})
62
+
63
+ # ── internals ────────────────────────────────────────────────────────────
64
+
65
+ def _dispatch(
66
+ self,
67
+ *,
68
+ input_str: str,
69
+ output_str: str,
70
+ model: str,
71
+ latency_ms: float,
72
+ status: str,
73
+ tools_used: Optional[List[str]] = None,
74
+ tokens: Optional[Dict[str, int]] = None,
75
+ error: Optional[str] = None,
76
+ session_id: Optional[str] = None,
77
+ metadata: Optional[Dict[str, Any]] = None,
78
+ ) -> None:
79
+ payload: Dict[str, Any] = {
80
+ "agentId": self.agent_id,
81
+ "sessionId": session_id or str(uuid.uuid4()),
82
+ "input": _extract.truncate(input_str, self.max_preview_length),
83
+ "output": _extract.truncate(output_str, self.max_preview_length),
84
+ "model": model or "unknown",
85
+ "latencyMs": max(0, min(MAX_LATENCY_MS, int(round(latency_ms)))),
86
+ "status": status,
87
+ "timestamp": _now_iso(),
88
+ }
89
+ merged_meta = {**self.metadata, **(metadata or {})}
90
+ if merged_meta:
91
+ payload["metadata"] = merged_meta
92
+ if tools_used:
93
+ payload["toolsUsed"] = tools_used
94
+ if tokens:
95
+ payload.update({k: v for k, v in tokens.items() if v is not None})
96
+ if error:
97
+ payload["error"] = _extract.truncate(error, 2000)
98
+ post_trace(self.endpoint, self.api_key, payload, self.timeout)
99
+
100
+ @staticmethod
101
+ def _input_of(args: tuple, kwargs: dict) -> Any:
102
+ if args:
103
+ return args[0]
104
+ if kwargs:
105
+ return next(iter(kwargs.values()))
106
+ return None
107
+
108
+ def _dispatch_success(
109
+ self, result: Any, args: tuple, kwargs: dict, start: float, metadata: Optional[Dict[str, Any]]
110
+ ) -> None:
111
+ tools = _extract.extract_tools(result)
112
+ self._dispatch(
113
+ input_str=_extract.safe_serialize(self._input_of(args, kwargs), self.max_preview_length),
114
+ output_str=_extract.extract_output(result, self.max_preview_length),
115
+ model=_extract.extract_model(result),
116
+ latency_ms=_elapsed_ms(start),
117
+ status="success",
118
+ tools_used=tools or None,
119
+ tokens=_extract.extract_tokens(result),
120
+ metadata=metadata,
121
+ )
122
+
123
+ # ── public API ───────────────────────────────────────────────────────────
124
+
125
+ def trace(
126
+ self,
127
+ *,
128
+ input: Any,
129
+ output: Any,
130
+ latency_ms: float,
131
+ model: str = "unknown",
132
+ status: str = "success",
133
+ tools_used: Optional[List[str]] = None,
134
+ prompt_tokens: Optional[int] = None,
135
+ completion_tokens: Optional[int] = None,
136
+ total_tokens: Optional[int] = None,
137
+ error: Optional[str] = None,
138
+ session_id: Optional[str] = None,
139
+ metadata: Optional[Dict[str, Any]] = None,
140
+ ) -> None:
141
+ """Manually submit a trace — for custom integrations you can't wrap."""
142
+ tokens = {
143
+ "promptTokens": prompt_tokens,
144
+ "completionTokens": completion_tokens,
145
+ "totalTokens": total_tokens,
146
+ }
147
+ self._dispatch(
148
+ input_str=_extract.safe_serialize(input, self.max_preview_length),
149
+ output_str=_extract.safe_serialize(output, self.max_preview_length),
150
+ model=model,
151
+ latency_ms=latency_ms,
152
+ status=status,
153
+ tools_used=tools_used,
154
+ tokens={k: v for k, v in tokens.items() if v is not None},
155
+ error=error,
156
+ session_id=session_id,
157
+ metadata=metadata,
158
+ )
159
+
160
+ def watch(
161
+ self,
162
+ fn: Optional[Callable] = None,
163
+ *,
164
+ metadata: Optional[Dict[str, Any]] = None,
165
+ ) -> Callable:
166
+ """Wrap a (sync or async) agent function to auto-capture traces.
167
+
168
+ Usable as `client.watch(fn)` or as a decorator `@client.watch()`.
169
+ Telemetry is fire-and-forget; errors are captured then re-raised so the
170
+ caller's behavior is unchanged.
171
+ """
172
+
173
+ def decorator(func: Callable) -> Callable:
174
+ if asyncio.iscoroutinefunction(func):
175
+
176
+ @functools.wraps(func)
177
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
178
+ start = time.perf_counter()
179
+ try:
180
+ result = await func(*args, **kwargs)
181
+ except Exception as err:
182
+ self._dispatch(
183
+ input_str=_extract.safe_serialize(
184
+ self._input_of(args, kwargs), self.max_preview_length
185
+ ),
186
+ output_str="",
187
+ model="unknown",
188
+ latency_ms=_elapsed_ms(start),
189
+ status="error",
190
+ error=str(err),
191
+ metadata=metadata,
192
+ )
193
+ raise
194
+ self._dispatch_success(result, args, kwargs, start, metadata)
195
+ return result
196
+
197
+ return async_wrapper
198
+
199
+ @functools.wraps(func)
200
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
201
+ start = time.perf_counter()
202
+ try:
203
+ result = func(*args, **kwargs)
204
+ except Exception as err:
205
+ self._dispatch(
206
+ input_str=_extract.safe_serialize(
207
+ self._input_of(args, kwargs), self.max_preview_length
208
+ ),
209
+ output_str="",
210
+ model="unknown",
211
+ latency_ms=_elapsed_ms(start),
212
+ status="error",
213
+ error=str(err),
214
+ metadata=metadata,
215
+ )
216
+ raise
217
+ self._dispatch_success(result, args, kwargs, start, metadata)
218
+ return result
219
+
220
+ return sync_wrapper
221
+
222
+ return decorator(fn) if fn is not None else decorator
223
+
224
+ def instrument_openai(self, openai_client: Any) -> Any:
225
+ """Monkeypatch an OpenAI-compatible client so every chat completion emits
226
+ a trace automatically — one line, no per-call wrapping.
227
+
228
+ Works for OpenAI, Groq, Cerebras, Together, vLLM — anything you drive
229
+ through the `openai` client with a different base_url. Idempotent.
230
+ Returns the same client for chaining.
231
+ """
232
+ completions = openai_client.chat.completions
233
+ if getattr(completions, "_agentwatch_instrumented", False):
234
+ return openai_client
235
+
236
+ original_create = completions.create
237
+ watcher = self
238
+
239
+ @functools.wraps(original_create)
240
+ def wrapped_create(*args: Any, **kwargs: Any) -> Any:
241
+ start = time.perf_counter()
242
+ messages = kwargs.get("messages")
243
+ input_str = (
244
+ _extract.input_from_messages(messages, watcher.max_preview_length)
245
+ if messages is not None
246
+ else ""
247
+ )
248
+ try:
249
+ response = original_create(*args, **kwargs)
250
+ except Exception as err:
251
+ watcher._dispatch(
252
+ input_str=input_str,
253
+ output_str="",
254
+ model=str(kwargs.get("model", "unknown")),
255
+ latency_ms=_elapsed_ms(start),
256
+ status="error",
257
+ error=str(err),
258
+ )
259
+ raise
260
+ watcher._dispatch(
261
+ input_str=input_str,
262
+ output_str=_extract.extract_output(response, watcher.max_preview_length),
263
+ model=_extract.extract_model(response) or str(kwargs.get("model", "unknown")),
264
+ latency_ms=_elapsed_ms(start),
265
+ status="success",
266
+ tools_used=_extract.extract_tools(response) or None,
267
+ tokens=_extract.extract_tokens(response),
268
+ )
269
+ return response
270
+
271
+ wrapped_create._agentwatch_original = original_create # type: ignore[attr-defined]
272
+ completions.create = wrapped_create # type: ignore[method-assign]
273
+ completions._agentwatch_instrumented = True # type: ignore[attr-defined]
274
+ return openai_client
275
+
276
+ def uninstrument_openai(self, openai_client: Any) -> Any:
277
+ """Undo instrument_openai() (mainly for tests)."""
278
+ completions = openai_client.chat.completions
279
+ original = getattr(completions.create, "_agentwatch_original", None)
280
+ if original is not None:
281
+ completions.create = original # type: ignore[method-assign]
282
+ completions._agentwatch_instrumented = False # type: ignore[attr-defined]
283
+ return openai_client
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentwatch-dev
3
+ Version: 0.1.0
4
+ Summary: Predict when production AI agents degrade — before they visibly fail. Python SDK.
5
+ Project-URL: Homepage, https://agentwatch.dev
6
+ Author: AgentWatch
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai-agents,cerebras,drift,groq,llm,monitoring,observability,openai
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.8
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == 'dev'
16
+ Provides-Extra: openai
17
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # AgentWatch — Python SDK
21
+
22
+ **Predict when production AI agents are degrading — before they visibly fail.**
23
+
24
+ AgentWatch watches your agent's traces and runs statistical change-point
25
+ detection (CUSUM/EWMA), an LLM-judge correctness gate, and semantic-drift scoring
26
+ **server-side** to catch quality collapse *before* your error rate moves. This SDK
27
+ just captures traces and ships them — it has **zero required dependencies** and
28
+ never adds latency to or raises errors in your agent's request path.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install agentwatch-dev
34
+ # optional, only for instrument_openai():
35
+ pip install "agentwatch-dev[openai]"
36
+ ```
37
+
38
+ The distribution is named `agentwatch-dev` (after agentwatch.dev); the import name
39
+ is just `agentwatch`.
40
+
41
+ ## Configure
42
+
43
+ ```python
44
+ import agentwatch
45
+ agentwatch.configure(api_key="aw_live_...", agent_id="<agent-uuid>")
46
+ ```
47
+
48
+ Self-hosting? Pass `endpoint="https://your-host"` to `configure(...)`.
49
+
50
+ ## Option A — auto-instrument (one line)
51
+
52
+ Works for **any OpenAI-compatible client** — OpenAI, Groq, Cerebras, Together,
53
+ vLLM — because they all use the `openai` client with a different `base_url`.
54
+
55
+ ```python
56
+ from openai import OpenAI
57
+
58
+ client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=GROQ_API_KEY)
59
+ agentwatch.instrument_openai(client)
60
+
61
+ # Every chat completion from here on is traced automatically:
62
+ client.chat.completions.create(
63
+ model="llama-3.3-70b-versatile",
64
+ messages=[{"role": "user", "content": "Refund order #47829"}],
65
+ )
66
+ ```
67
+
68
+ ## Option B — wrap your agent function
69
+
70
+ ```python
71
+ @agentwatch.watch()
72
+ def run_agent(query: str) -> str:
73
+ ...
74
+ return answer
75
+
76
+ # async is supported too:
77
+ @agentwatch.watch()
78
+ async def run_agent_async(query: str): ...
79
+ ```
80
+
81
+ `watch()` captures the first argument as the input and the return value as the
82
+ output. If the return value is an OpenAI-shaped response, it also extracts the
83
+ model, token counts, and tool names automatically.
84
+
85
+ ## Option C — manual trace
86
+
87
+ ```python
88
+ client = agentwatch.AgentWatchClient(api_key="aw_live_...", agent_id="<uuid>")
89
+ client.trace(
90
+ input="Refund order #47829",
91
+ output="I've issued the refund…",
92
+ model="llama-3.3-70b-versatile",
93
+ latency_ms=812,
94
+ prompt_tokens=220, completion_tokens=180, total_tokens=400,
95
+ tools_used=["search_kb", "issue_refund"],
96
+ )
97
+ ```
98
+
99
+ ## Notes
100
+
101
+ - **Fire-and-forget:** traces are POSTed on a daemon thread; delivery failures are
102
+ swallowed so monitoring can never break your agent.
103
+ - **Metadata:** attach tags via `configure(..., metadata={"env": "prod"})` or
104
+ per-call `@agentwatch.watch(metadata={"version": "2.1"})`.
105
+ - **Streaming** (`stream=True`) responses aren't fully captured yet — the trace is
106
+ still recorded, but token/output extraction may be partial.
107
+
108
+ MIT licensed.
@@ -0,0 +1,8 @@
1
+ agentwatch/__init__.py,sha256=eaSLdz6HQGADR1r1NqVMr4yWroHvN2SO1xFO7z7kaDs,2423
2
+ agentwatch/_extract.py,sha256=f7y-CEqsdUsqwOFwzOm-pxEagM9n1oRwnNOxTMyRFrw,3350
3
+ agentwatch/_transport.py,sha256=37LWah05wSAoNHmGEQiDDcx5TH4eUda-dJdhcMa4lkA,1283
4
+ agentwatch/client.py,sha256=rVPMVoaLLVT_KFM2G9rcqjeB3G5yP3fNgQ0VjWaQTDk,11234
5
+ agentwatch_dev-0.1.0.dist-info/METADATA,sha256=p4qFOugmaNUgUoZg4oHRpu9EwXAsH0O_gFJIfPwWGtg,3429
6
+ agentwatch_dev-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
7
+ agentwatch_dev-0.1.0.dist-info/licenses/LICENSE,sha256=sNFI0vIuwEXtlgwJktNQrqEh55kX9p63RuwnHX8Z0ps,1066
8
+ agentwatch_dev-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentWatch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING IN THE
21
+ SOFTWARE.