niitaka-sdk 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.
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .venv/
6
+ venv/
7
+ *.egg-info/
8
+
9
+ # Environment / secrets — never commit these
10
+ .env
11
+ .env.local
12
+ .env.*.local
13
+ agents/.env
14
+ backend/.env
15
+ backend/.env.dev
16
+ backend/.env.prod
17
+
18
+ # Node / Next.js
19
+ node_modules/
20
+ .next/
21
+ *.tsbuildinfo
22
+ frontend/next-env.d.ts
23
+ frontend/package-lock.json
24
+
25
+ # macOS
26
+ .DS_Store
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: niitaka-sdk
3
+ Version: 0.1.0
4
+ Summary: AI agent observability and control plane SDK
5
+ Project-URL: Homepage, https://niitaka.ai
6
+ Project-URL: Docs, https://docs.niitaka.ai
7
+ Project-URL: Repository, https://github.com/amigotomodachiami/Niitaka
8
+ Author-email: Niitaka <hello@niitaka.ai>
9
+ License: MIT
10
+ Keywords: agents,ai,anthropic,gemini,groq,langchain,llm,observability,openai,tracing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx>=0.24
23
+ Requires-Dist: requests>=2.28
24
+ Provides-Extra: all
25
+ Requires-Dist: anthropic>=0.20; extra == 'all'
26
+ Requires-Dist: google-generativeai>=0.4; extra == 'all'
27
+ Requires-Dist: groq>=0.4; extra == 'all'
28
+ Requires-Dist: langchain-core>=0.1; extra == 'all'
29
+ Requires-Dist: openai>=1.0; extra == 'all'
30
+ Provides-Extra: anthropic
31
+ Requires-Dist: anthropic>=0.20; extra == 'anthropic'
32
+ Provides-Extra: gemini
33
+ Requires-Dist: google-generativeai>=0.4; extra == 'gemini'
34
+ Provides-Extra: groq
35
+ Requires-Dist: groq>=0.4; extra == 'groq'
36
+ Provides-Extra: langchain
37
+ Requires-Dist: langchain-core>=0.1; extra == 'langchain'
38
+ Provides-Extra: openai
39
+ Requires-Dist: openai>=1.0; extra == 'openai'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # Niitaka SDK
43
+
44
+ **AI agent observability and control plane — one import away.**
45
+
46
+ Niitaka instruments your AI agents to capture every LLM call, tool invocation, and decision trace. Connect to the [Niitaka dashboard](https://niitaka.ai) to run experiments, enforce guardrails, and version your agents — without changing your agent code.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install niitaka-sdk # core
52
+ pip install "niitaka-sdk[openai]" # + OpenAI
53
+ pip install "niitaka-sdk[anthropic]" # + Anthropic
54
+ pip install "niitaka-sdk[all]" # all providers
55
+ ```
56
+
57
+ ## Quick start
58
+
59
+ ```python
60
+ import niitaka
61
+ import os
62
+
63
+ niitaka.configure(
64
+ api_key=os.getenv("NIITAKA_API_KEY"),
65
+ api_url="https://api.niitaka.ai", # or your self-hosted URL
66
+ )
67
+
68
+ # Instrument your LLM provider (pick one or more)
69
+ niitaka.instrument_openai()
70
+ niitaka.instrument_anthropic()
71
+ niitaka.instrument_gemini()
72
+ niitaka.instrument_groq()
73
+
74
+ # Wrap your agent run in a session
75
+ with niitaka.start_session(goal="Summarise this document", agent_id="my-agent"):
76
+ response = client.chat.completions.create(...) # auto-logged
77
+ ```
78
+
79
+ Every call inside `start_session` is automatically traced — latency, token usage, cost, errors, and tool calls.
80
+
81
+ ## Providers
82
+
83
+ | Provider | Extra | Import |
84
+ |---|---|---|
85
+ | OpenAI | `niitaka-sdk[openai]` | `niitaka.instrument_openai()` |
86
+ | Anthropic | `niitaka-sdk[anthropic]` | `niitaka.instrument_anthropic()` |
87
+ | Google Gemini | `niitaka-sdk[gemini]` | `niitaka.instrument_gemini()` |
88
+ | Groq | `niitaka-sdk[groq]` | `niitaka.instrument_groq()` |
89
+ | LangChain | `niitaka-sdk[langchain]` | `from niitaka import NiitakaCallbackHandler` |
90
+
91
+ ## LangChain
92
+
93
+ ```python
94
+ from niitaka import NiitakaCallbackHandler
95
+ import niitaka
96
+
97
+ with niitaka.start_session(goal="...", agent_id="lc-agent"):
98
+ chain.invoke({"input": "..."}, config={"callbacks": [NiitakaCallbackHandler()]})
99
+ ```
100
+
101
+ ## Runtime config
102
+
103
+ Niitaka can push model, temperature, and guardrail config to your agent at runtime — no redeploy needed:
104
+
105
+ ```python
106
+ config = niitaka.get_runtime_config(agent_id="my-agent")
107
+ # config["llm"]["model"], config["guardrails"]["cost_limit_usd"], ...
108
+ ```
109
+
110
+ ## Links
111
+
112
+ - **Dashboard**: [niitaka.ai](https://niitaka.ai)
113
+ - **Docs**: [docs.niitaka.ai](https://docs.niitaka.ai)
114
+ - **Support**: hello@niitaka.ai
@@ -0,0 +1,73 @@
1
+ # Niitaka SDK
2
+
3
+ **AI agent observability and control plane — one import away.**
4
+
5
+ Niitaka instruments your AI agents to capture every LLM call, tool invocation, and decision trace. Connect to the [Niitaka dashboard](https://niitaka.ai) to run experiments, enforce guardrails, and version your agents — without changing your agent code.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install niitaka-sdk # core
11
+ pip install "niitaka-sdk[openai]" # + OpenAI
12
+ pip install "niitaka-sdk[anthropic]" # + Anthropic
13
+ pip install "niitaka-sdk[all]" # all providers
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ import niitaka
20
+ import os
21
+
22
+ niitaka.configure(
23
+ api_key=os.getenv("NIITAKA_API_KEY"),
24
+ api_url="https://api.niitaka.ai", # or your self-hosted URL
25
+ )
26
+
27
+ # Instrument your LLM provider (pick one or more)
28
+ niitaka.instrument_openai()
29
+ niitaka.instrument_anthropic()
30
+ niitaka.instrument_gemini()
31
+ niitaka.instrument_groq()
32
+
33
+ # Wrap your agent run in a session
34
+ with niitaka.start_session(goal="Summarise this document", agent_id="my-agent"):
35
+ response = client.chat.completions.create(...) # auto-logged
36
+ ```
37
+
38
+ Every call inside `start_session` is automatically traced — latency, token usage, cost, errors, and tool calls.
39
+
40
+ ## Providers
41
+
42
+ | Provider | Extra | Import |
43
+ |---|---|---|
44
+ | OpenAI | `niitaka-sdk[openai]` | `niitaka.instrument_openai()` |
45
+ | Anthropic | `niitaka-sdk[anthropic]` | `niitaka.instrument_anthropic()` |
46
+ | Google Gemini | `niitaka-sdk[gemini]` | `niitaka.instrument_gemini()` |
47
+ | Groq | `niitaka-sdk[groq]` | `niitaka.instrument_groq()` |
48
+ | LangChain | `niitaka-sdk[langchain]` | `from niitaka import NiitakaCallbackHandler` |
49
+
50
+ ## LangChain
51
+
52
+ ```python
53
+ from niitaka import NiitakaCallbackHandler
54
+ import niitaka
55
+
56
+ with niitaka.start_session(goal="...", agent_id="lc-agent"):
57
+ chain.invoke({"input": "..."}, config={"callbacks": [NiitakaCallbackHandler()]})
58
+ ```
59
+
60
+ ## Runtime config
61
+
62
+ Niitaka can push model, temperature, and guardrail config to your agent at runtime — no redeploy needed:
63
+
64
+ ```python
65
+ config = niitaka.get_runtime_config(agent_id="my-agent")
66
+ # config["llm"]["model"], config["guardrails"]["cost_limit_usd"], ...
67
+ ```
68
+
69
+ ## Links
70
+
71
+ - **Dashboard**: [niitaka.ai](https://niitaka.ai)
72
+ - **Docs**: [docs.niitaka.ai](https://docs.niitaka.ai)
73
+ - **Support**: hello@niitaka.ai
@@ -0,0 +1,26 @@
1
+ from .instrumentation import (
2
+ instrument_openai,
3
+ instrument_anthropic,
4
+ instrument_gemini,
5
+ instrument_groq,
6
+ )
7
+ from .client import get_runtime_config, start_session, configure
8
+
9
+ # NiitakaCallbackHandler is optional — only available when langchain-core is installed.
10
+ # Import it directly: from sdk.langchain_handler import NiitakaCallbackHandler
11
+ try:
12
+ from .langchain_handler import NiitakaCallbackHandler
13
+ except ImportError:
14
+ pass
15
+
16
+
17
+ def auto_instrument():
18
+ """Instrument all installed LLM providers.
19
+
20
+ Calls each provider's instrument function and silently skips any that
21
+ are not installed. Equivalent to calling each function individually.
22
+ """
23
+ instrument_openai()
24
+ instrument_anthropic()
25
+ instrument_gemini()
26
+ instrument_groq()
@@ -0,0 +1,278 @@
1
+ """Niitaka CLI — manage policy files.
2
+
3
+ Usage:
4
+ python -m sdk.cli push <policy-file> [--url URL] [--key KEY] [--dry-run]
5
+ python -m sdk.cli pull [--agent AGENT_ID] [--url URL] [--key KEY] [-o FILE]
6
+ python -m sdk.cli validate <policy-file>
7
+ python -m sdk.cli history [--agent AGENT_ID] [--limit N] [--url URL] [--key KEY]
8
+
9
+ Commands
10
+ --------
11
+ push Upload policies from a YAML/JSON file to the backend DB.
12
+ By default replaces all existing policies for each agent in the file.
13
+
14
+ pull Download current DB policies for one or all agents and print as YAML.
15
+ Use -o to write to a file instead of stdout.
16
+
17
+ validate Parse and validate a policy file without contacting the backend.
18
+
19
+ history Show the policy audit log — who changed what and when.
20
+ Use --agent to filter to a specific agent.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import os
27
+ import sys
28
+
29
+ import requests
30
+
31
+
32
+ # ── Helpers ───────────────────────────────────────────────────────────────────
33
+
34
+ def _headers(key: str) -> dict:
35
+ h = {"Content-Type": "application/json"}
36
+ if key:
37
+ h["X-API-Key"] = key
38
+ return h
39
+
40
+
41
+ def _base_url(url: str) -> str:
42
+ return url.rstrip("/")
43
+
44
+
45
+ # ── Commands ──────────────────────────────────────────────────────────────────
46
+
47
+ def cmd_validate(args: argparse.Namespace) -> int:
48
+ from sdk.policy_loader import load_policy_file, PolicyFileError
49
+ try:
50
+ policies = load_policy_file(args.file)
51
+ except PolicyFileError as exc:
52
+ print(f"[ERROR] {exc}", file=sys.stderr)
53
+ return 1
54
+
55
+ from collections import Counter
56
+ counts = Counter(p["agent_id"] for p in policies)
57
+ print(f"✓ Valid policy file — {len(policies)} policies across {len(counts)} agent(s):")
58
+ for agent_id, n in sorted(counts.items()):
59
+ print(f" {agent_id}: {n} rule(s)")
60
+ return 0
61
+
62
+
63
+ def cmd_push(args: argparse.Namespace) -> int:
64
+ from sdk.policy_loader import load_policy_file, group_by_agent, PolicyFileError
65
+
66
+ # Validate file first
67
+ try:
68
+ policies = load_policy_file(args.file)
69
+ except PolicyFileError as exc:
70
+ print(f"[ERROR] {exc}", file=sys.stderr)
71
+ return 1
72
+
73
+ grouped = group_by_agent(policies)
74
+ base = _base_url(args.url)
75
+ headers = _headers(args.key)
76
+
77
+ print(f"[niitaka push] {args.file} → {base}")
78
+ print(f" Agents: {', '.join(sorted(grouped))}")
79
+
80
+ if args.dry_run:
81
+ print(" [dry-run] No changes made.")
82
+ return 0
83
+
84
+ # Build import payload: list of policy objects with agent_id + config
85
+ payload_policies = []
86
+ for p in policies:
87
+ payload_policies.append({
88
+ "agent_id": p["agent_id"],
89
+ "type": p["type"],
90
+ "config": {
91
+ "condition": p["condition"],
92
+ "action": p["action"],
93
+ "priority": p["priority"],
94
+ },
95
+ })
96
+
97
+ try:
98
+ resp = requests.post(
99
+ f"{base}/policies/import",
100
+ headers=headers,
101
+ json={"policies": payload_policies, "replace": True},
102
+ timeout=15,
103
+ )
104
+ resp.raise_for_status()
105
+ except requests.HTTPError as exc:
106
+ print(f"[ERROR] Backend returned {exc.response.status_code}: {exc.response.text}",
107
+ file=sys.stderr)
108
+ return 1
109
+ except requests.RequestException as exc:
110
+ print(f"[ERROR] Could not reach backend at {base}: {exc}", file=sys.stderr)
111
+ return 1
112
+
113
+ result = resp.json()
114
+ print(f" ✓ Imported {result.get('imported', '?')} policies "
115
+ f"(replaced {result.get('deleted', '?')} existing)")
116
+ for agent_id, n in sorted(result.get("by_agent", {}).items()):
117
+ print(f" {agent_id}: {n} rule(s) written")
118
+ return 0
119
+
120
+
121
+ def cmd_pull(args: argparse.Namespace) -> int:
122
+ from sdk.policy_loader import to_yaml_string
123
+
124
+ base = _base_url(args.url)
125
+ headers = _headers(args.key)
126
+
127
+ if args.agent:
128
+ # Single agent
129
+ try:
130
+ resp = requests.get(f"{base}/policies/{args.agent}", headers=headers, timeout=10)
131
+ resp.raise_for_status()
132
+ except requests.RequestException as exc:
133
+ print(f"[ERROR] {exc}", file=sys.stderr)
134
+ return 1
135
+ raw = resp.json()
136
+ # Attach agent_id (not in per-agent response)
137
+ for p in raw:
138
+ p["agent_id"] = args.agent
139
+ else:
140
+ # All policies
141
+ try:
142
+ resp = requests.get(f"{base}/policies", headers=headers, timeout=10)
143
+ resp.raise_for_status()
144
+ except requests.RequestException as exc:
145
+ print(f"[ERROR] {exc}", file=sys.stderr)
146
+ return 1
147
+ raw = resp.json()
148
+
149
+ if not raw:
150
+ print("# No policies found.", file=sys.stderr)
151
+ return 0
152
+
153
+ yaml_text = to_yaml_string(raw)
154
+
155
+ if args.output:
156
+ with open(args.output, "w", encoding="utf-8") as fh:
157
+ fh.write(yaml_text)
158
+ print(f"[niitaka pull] Wrote {len(raw)} policies to {args.output}")
159
+ else:
160
+ sys.stdout.write(yaml_text)
161
+
162
+ return 0
163
+
164
+
165
+ def cmd_history(args: argparse.Namespace) -> int:
166
+ base = _base_url(args.url)
167
+ headers = _headers(args.key)
168
+
169
+ url = f"{base}/policies/audit"
170
+ if args.agent:
171
+ url = f"{base}/policies/audit/{args.agent}"
172
+ url += f"?limit={args.limit}"
173
+
174
+ try:
175
+ resp = requests.get(url, headers=headers, timeout=10)
176
+ resp.raise_for_status()
177
+ except requests.RequestException as exc:
178
+ print(f"[ERROR] {exc}", file=sys.stderr)
179
+ return 1
180
+
181
+ entries = resp.json()
182
+ if not entries:
183
+ print("No audit log entries found.")
184
+ return 0
185
+
186
+ # Column widths
187
+ W_TIME = 20
188
+ W_AGENT = 20
189
+ W_ACTION = 8
190
+ W_TYPE = 12
191
+ W_SOURCE = 6
192
+
193
+ header = (
194
+ f"{'Timestamp':<{W_TIME}} "
195
+ f"{'Agent':<{W_AGENT}} "
196
+ f"{'Action':<{W_ACTION}} "
197
+ f"{'Type':<{W_TYPE}} "
198
+ f"{'Src':<{W_SOURCE}}"
199
+ )
200
+ print(header)
201
+ print("─" * len(header))
202
+
203
+ for e in entries:
204
+ ts = e.get("created_at", "")
205
+ # Trim to readable format: "2026-04-12T14:32:01"
206
+ if ts and "T" in ts:
207
+ ts = ts[:19].replace("T", " ")
208
+
209
+ action = e.get("action", "")
210
+ agent = (e.get("agent_id") or "")[:W_AGENT]
211
+ ptype = (e.get("policy_type") or "—")[:W_TYPE]
212
+ source = (e.get("source") or "")[:W_SOURCE]
213
+
214
+ print(
215
+ f"{ts:<{W_TIME}} "
216
+ f"{agent:<{W_AGENT}} "
217
+ f"{action:<{W_ACTION}} "
218
+ f"{ptype:<{W_TYPE}} "
219
+ f"{source:<{W_SOURCE}}"
220
+ )
221
+
222
+ print(f"\n{len(entries)} entries")
223
+ return 0
224
+
225
+
226
+ # ── Argument parser ───────────────────────────────────────────────────────────
227
+
228
+ def build_parser() -> argparse.ArgumentParser:
229
+ parser = argparse.ArgumentParser(
230
+ prog="python -m sdk.cli",
231
+ description="Niitaka policy file manager",
232
+ )
233
+ sub = parser.add_subparsers(dest="command", required=True)
234
+
235
+ # Shared backend args
236
+ def add_backend_args(p: argparse.ArgumentParser) -> None:
237
+ p.add_argument("--url", default=os.getenv("NIITAKA_BACKEND_URL", "http://localhost:8000"),
238
+ help="Backend URL (default: $NIITAKA_BACKEND_URL or http://localhost:8000)")
239
+ p.add_argument("--key", default=os.getenv("NIITAKA_API_KEY", ""),
240
+ help="API key (default: $NIITAKA_API_KEY)")
241
+
242
+ # validate
243
+ p_val = sub.add_parser("validate", help="Validate a policy file without hitting the backend")
244
+ p_val.add_argument("file", help="Path to policy YAML or JSON file")
245
+
246
+ # push
247
+ p_push = sub.add_parser("push", help="Upload policy file to backend DB")
248
+ p_push.add_argument("file", help="Path to policy YAML or JSON file")
249
+ p_push.add_argument("--dry-run", action="store_true",
250
+ help="Validate and show what would be pushed, but make no changes")
251
+ add_backend_args(p_push)
252
+
253
+ # pull
254
+ p_pull = sub.add_parser("pull", help="Download policies from backend as YAML")
255
+ p_pull.add_argument("--agent", default=None, help="Filter to a single agent_id")
256
+ p_pull.add_argument("-o", "--output", default=None, help="Write to file instead of stdout")
257
+ add_backend_args(p_pull)
258
+
259
+ # history
260
+ p_hist = sub.add_parser("history", help="Show the policy audit log")
261
+ p_hist.add_argument("--agent", default=None, help="Filter to a single agent_id")
262
+ p_hist.add_argument("--limit", type=int, default=50,
263
+ help="Max entries to show (default: 50)")
264
+ add_backend_args(p_hist)
265
+
266
+ return parser
267
+
268
+
269
+ def main() -> None:
270
+ parser = build_parser()
271
+ args = parser.parse_args()
272
+
273
+ dispatch = {"validate": cmd_validate, "push": cmd_push, "pull": cmd_pull, "history": cmd_history}
274
+ sys.exit(dispatch[args.command](args))
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()