cortexops 0.1.0__tar.gz → 0.2.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.
Files changed (32) hide show
  1. {cortexops-0.1.0 → cortexops-0.2.0}/.gitignore +33 -33
  2. {cortexops-0.1.0 → cortexops-0.2.0}/PKG-INFO +5 -6
  3. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/__init__.py +9 -3
  4. cortexops-0.2.0/cortexops/auth.py +149 -0
  5. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/cli.py +40 -2
  6. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/client.py +0 -1
  7. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/judge.py +0 -1
  8. {cortexops-0.1.0/cortexops → cortexops-0.2.0}/cortexops/metrics.py +1 -2
  9. {cortexops-0.1.0/cortexops → cortexops-0.2.0}/cortexops/models.py +3 -0
  10. {cortexops-0.1.0/cortexops → cortexops-0.2.0}/cortexops/tracer.py +99 -31
  11. {cortexops-0.1.0 → cortexops-0.2.0}/pyproject.toml +7 -8
  12. cortexops-0.2.0/tests/conftest.py +8 -0
  13. {cortexops-0.1.0 → cortexops-0.2.0}/tests/test_enhancements.py +3 -1
  14. cortexops-0.1.0/cortexops/cortexops/__init__.py +0 -58
  15. cortexops-0.1.0/cortexops/cortexops/cli.py +0 -195
  16. cortexops-0.1.0/cortexops/cortexops/client.py +0 -84
  17. cortexops-0.1.0/cortexops/cortexops/judge.py +0 -155
  18. cortexops-0.1.0/cortexops/eval.py +0 -216
  19. cortexops-0.1.0/cortexops/metrics.py +0 -184
  20. cortexops-0.1.0/cortexops/models.py +0 -141
  21. cortexops-0.1.0/cortexops/tests/test_enhancements.py +0 -222
  22. cortexops-0.1.0/cortexops/tracer.py +0 -210
  23. cortexops-0.1.0/tests/__init__.py +0 -0
  24. cortexops-0.1.0/tests/test_cortexops.py +0 -211
  25. {cortexops-0.1.0 → cortexops-0.2.0}/LICENSE +0 -0
  26. {cortexops-0.1.0 → cortexops-0.2.0}/README.md +0 -0
  27. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/LICENSE +0 -0
  28. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/README.md +0 -0
  29. {cortexops-0.1.0/cortexops → cortexops-0.2.0}/cortexops/eval.py +0 -0
  30. {cortexops-0.1.0 → cortexops-0.2.0}/cortexops/pyproject.toml +0 -0
  31. {cortexops-0.1.0/cortexops → cortexops-0.2.0}/tests/__init__.py +0 -0
  32. {cortexops-0.1.0/cortexops → cortexops-0.2.0}/tests/test_cortexops.py +0 -0
@@ -1,34 +1,34 @@
1
- # Python
2
- __pycache__/
3
- *.py[cod]
4
- *.pyo
5
- .venv/
6
- venv/
7
- .env
8
- *.egg-info/
9
- dist/
10
- build/
11
- PKG-INFO
12
- *.whl
13
- *.tar.gz
14
-
15
- # Test / lint caches
16
- .pytest_cache/
17
- .ruff_cache/
18
- .mypy_cache/
19
-
20
- # Package managers
21
- uv.lock
22
- .python-version
23
-
24
- # Database
25
- *.db
26
- *.sqlite
27
-
28
- # IDE
29
- .vscode/
30
- .idea/
31
-
32
- # OS
33
- .DS_Store
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ .venv/
6
+ venv/
7
+ .env
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ PKG-INFO
12
+ *.whl
13
+ *.tar.gz
14
+
15
+ # Test / lint caches
16
+ .pytest_cache/
17
+ .ruff_cache/
18
+ .mypy_cache/
19
+
20
+ # Package managers
21
+ uv.lock
22
+ .python-version
23
+
24
+ # Database
25
+ *.db
26
+ *.sqlite
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+
32
+ # OS
33
+ .DS_Store
34
34
  Thumbs.db
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexops
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Reliability infrastructure for AI agents — evaluation, observability, and regression testing
5
- Project-URL: Homepage, https://cortexops.ai
5
+ Project-URL: Homepage, https://getcortexops.com
6
6
  Project-URL: Repository, https://github.com/ashishodu2023/cortexops
7
- Project-URL: Documentation, https://docs.cortexops.ai
7
+ Project-URL: Documentation, https://docs.getcortexops.com
8
8
  Project-URL: Bug Tracker, https://github.com/ashishodu2023/cortexops/issues
9
9
  Project-URL: Changelog, https://github.com/ashishodu2023/cortexops/releases
10
- Author-email: Ashish <ashishodu2023@gmail.com>
10
+ Author-email: Ashish <ashish@getcortexops.com>
11
11
  License: MIT License
12
12
 
13
13
  Copyright (c) 2025 CortexOps Contributors
@@ -31,7 +31,7 @@ License: MIT License
31
31
  SOFTWARE.
32
32
  License-File: LICENSE
33
33
  Keywords: agents,ai,autogen,crewai,evaluation,langgraph,llm,observability,testing
34
- Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Development Status :: 4 - Beta
35
35
  Classifier: Intended Audience :: Developers
36
36
  Classifier: License :: OSI Approved :: MIT License
37
37
  Classifier: Operating System :: OS Independent
@@ -46,7 +46,6 @@ Classifier: Typing :: Typed
46
46
  Requires-Python: >=3.10
47
47
  Requires-Dist: pydantic>=2.0
48
48
  Requires-Dist: pyyaml>=6.0
49
- Requires-Dist: setuptools>=82.0.1
50
49
  Provides-Extra: all
51
50
  Requires-Dist: httpx>=0.27; extra == 'all'
52
51
  Provides-Extra: dev
@@ -27,13 +27,14 @@ from .models import (
27
27
  EvalSummary,
28
28
  FailureKind,
29
29
  RunStatus,
30
+ ToolCall,
30
31
  Trace,
31
32
  TraceNode,
32
- ToolCall,
33
33
  )
34
+ from .auth import cmd_login, cmd_logout, cmd_whoami, save_credentials, load_credentials
34
35
  from .tracer import CortexTracer
35
36
 
36
- __version__ = "0.1.0"
37
+ __version__ = "0.2.0"
37
38
 
38
39
  __all__ = [
39
40
  "CortexTracer",
@@ -55,4 +56,9 @@ __all__ = [
55
56
  "CaseResult",
56
57
  "FailureKind",
57
58
  "RunStatus",
58
- ]
59
+ "cmd_login",
60
+ "cmd_logout",
61
+ "cmd_whoami",
62
+ "save_credentials",
63
+ "load_credentials",
64
+ ]
@@ -0,0 +1,149 @@
1
+ """
2
+ cortexops login / logout / whoami — credential management.
3
+ Stores API key in ~/.cortexops/credentials (JSON).
4
+ Called by CLI and importable for programmatic use.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ _CREDENTIALS_DIR = Path.home() / ".cortexops"
14
+ _CREDENTIALS_FILE = _CREDENTIALS_DIR / "credentials"
15
+ _DEFAULT_API_URL = "https://api.getcortexops.com"
16
+
17
+
18
+ def save_credentials(api_key: str, project: str, api_url: str = _DEFAULT_API_URL) -> None:
19
+ """Persist credentials to ~/.cortexops/credentials."""
20
+ _CREDENTIALS_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
21
+ creds = {"api_key": api_key, "project": project, "api_url": api_url}
22
+ _CREDENTIALS_FILE.write_text(json.dumps(creds, indent=2))
23
+ _CREDENTIALS_FILE.chmod(0o600) # owner read/write only — no secrets leak
24
+
25
+
26
+ def load_credentials() -> dict | None:
27
+ """Load credentials from ~/.cortexops/credentials. Returns None if not found."""
28
+ if not _CREDENTIALS_FILE.exists():
29
+ return None
30
+ try:
31
+ return json.loads(_CREDENTIALS_FILE.read_text())
32
+ except Exception:
33
+ return None
34
+
35
+
36
+ def clear_credentials() -> None:
37
+ """Remove stored credentials."""
38
+ if _CREDENTIALS_FILE.exists():
39
+ _CREDENTIALS_FILE.unlink()
40
+
41
+
42
+ def verify_key(api_key: str, api_url: str = _DEFAULT_API_URL) -> dict | None:
43
+ """
44
+ Call GET /health with the key to verify it is valid.
45
+ Returns {"project": ..., "environment": ...} on success, None on failure.
46
+ """
47
+ try:
48
+ import httpx
49
+ r = httpx.get(
50
+ f"{api_url.rstrip('/')}/health",
51
+ headers={"X-API-Key": api_key},
52
+ timeout=8.0,
53
+ )
54
+ if r.status_code == 200:
55
+ return r.json()
56
+ return None
57
+ except Exception:
58
+ return None
59
+
60
+
61
+ def cmd_login(api_key: str | None = None, project: str | None = None,
62
+ api_url: str = _DEFAULT_API_URL) -> int:
63
+ """
64
+ Interactive login flow. Called by `cortexops login`.
65
+
66
+ If api_key is not provided, prompts interactively.
67
+ Returns 0 on success, 1 on failure.
68
+ """
69
+ print("CortexOps Login")
70
+ print("Get your API key at https://getcortexops.com/#pricing\n")
71
+
72
+ if not api_key:
73
+ try:
74
+ import getpass
75
+ api_key = getpass.getpass("API key (cxo-...): ").strip()
76
+ except KeyboardInterrupt:
77
+ print("\nCancelled.")
78
+ return 1
79
+
80
+ if not api_key.startswith("cxo-"):
81
+ print("Error: API key must start with 'cxo-'", file=sys.stderr)
82
+ return 1
83
+
84
+ if not project:
85
+ project = input("Default project name: ").strip() or "my-agent"
86
+
87
+ print(f"\nVerifying key against {api_url}...")
88
+ info = verify_key(api_key, api_url)
89
+ if info is None:
90
+ print(
91
+ "Warning: Could not verify key (API may be unreachable).\n"
92
+ "Saving credentials anyway — verify manually with: cortexops whoami",
93
+ file=sys.stderr,
94
+ )
95
+
96
+ save_credentials(api_key, project, api_url)
97
+ masked = api_key[:8] + "..." + api_key[-4:]
98
+ print("\n✓ Logged in")
99
+ print(f" Key: {masked}")
100
+ print(f" Project: {project}")
101
+ print(" Stored: ~/.cortexops/credentials")
102
+ print("\nYou can now use CortexTracer without passing api_key:")
103
+ print(f" tracer = CortexTracer(project=\"{project}\")")
104
+ return 0
105
+
106
+
107
+ def cmd_logout() -> int:
108
+ """Called by `cortexops logout`."""
109
+ creds = load_credentials()
110
+ if not creds:
111
+ print("Not logged in.")
112
+ return 0
113
+ clear_credentials()
114
+ print("✓ Logged out — credentials removed from ~/.cortexops/credentials")
115
+ return 0
116
+
117
+
118
+ def cmd_whoami(api_url: str | None = None) -> int:
119
+ """Called by `cortexops whoami`."""
120
+ # Check env var first
121
+ env_key = os.getenv("CORTEXOPS_API_KEY")
122
+ file_creds = load_credentials()
123
+
124
+ if not env_key and not file_creds:
125
+ print("Not logged in.\nRun: cortexops login", file=sys.stderr)
126
+ return 1
127
+
128
+ if env_key:
129
+ print("API key source : CORTEXOPS_API_KEY (env)")
130
+ masked = env_key[:8] + "..." + env_key[-4:]
131
+ print(f"Key : {masked}")
132
+ url = api_url or os.getenv("CORTEXOPS_API_URL", _DEFAULT_API_URL)
133
+ else:
134
+ masked = file_creds["api_key"][:8] + "..." + file_creds["api_key"][-4:]
135
+ print("API key source : ~/.cortexops/credentials")
136
+ print(f"Key : {masked}")
137
+ print(f"Project : {file_creds.get('project', '—')}")
138
+ url = api_url or file_creds.get("api_url", _DEFAULT_API_URL)
139
+ env_key = file_creds["api_key"]
140
+
141
+ print(f"API URL : {url}")
142
+ print("\nVerifying...")
143
+ info = verify_key(env_key, url)
144
+ if info:
145
+ print(f"✓ Key is valid (API status: {info.get('status', 'ok')})")
146
+ else:
147
+ print("✗ Key verification failed — API unreachable or key invalid")
148
+ return 1
149
+ return 0
@@ -76,7 +76,8 @@ def cmd_eval_diff(args: argparse.Namespace) -> int:
76
76
  regressions = diff.get("regressions", [])
77
77
  improvements = diff.get("improvements", [])
78
78
 
79
- sign = lambda v: f"+{v:.1%}" if v >= 0 else f"{v:.1%}"
79
+ def sign(v):
80
+ return f"+{v:.1%}" if v >= 0 else f"{v:.1%}"
80
81
  print(f"Diff: {args.run_a[:8]} → {args.run_b[:8]}")
81
82
  print(f" Task completion : {sign(delta_tc)}")
82
83
  print(f" Tool accuracy : {sign(delta_tool / 100)}")
@@ -137,6 +138,29 @@ def _load_agent(agent_path: str):
137
138
  return getattr(module, attr)
138
139
 
139
140
 
141
+
142
+
143
+ def cmd_login(args: argparse.Namespace) -> int:
144
+ """cortexops login"""
145
+ from cortexops.auth import cmd_login as _login
146
+ return _login(
147
+ api_key=getattr(args, 'api_key', None),
148
+ project=getattr(args, 'project', None),
149
+ api_url=getattr(args, 'base_url', 'https://api.getcortexops.com'),
150
+ )
151
+
152
+
153
+ def cmd_logout(args: argparse.Namespace) -> int:
154
+ """cortexops logout"""
155
+ from cortexops.auth import cmd_logout as _logout
156
+ return _logout()
157
+
158
+
159
+ def cmd_whoami(args: argparse.Namespace) -> int:
160
+ """cortexops whoami"""
161
+ from cortexops.auth import cmd_whoami as _whoami
162
+ return _whoami(api_url=getattr(args, 'base_url', None))
163
+
140
164
  def main() -> None:
141
165
  parser = argparse.ArgumentParser(
142
166
  prog="cortexops",
@@ -169,12 +193,26 @@ def main() -> None:
169
193
  fail_p.add_argument("--api-key", default=None)
170
194
  fail_p.add_argument("--base-url", default="https://api.cortexops.ai")
171
195
 
196
+
197
+ # ── login / logout / whoami ───────────────────────────────────────────
198
+ login_p = sub.add_parser("login", help="Save API key to ~/.cortexops/credentials")
199
+ login_p.add_argument("--api-key", default=None, help="cxo-... key (prompted if omitted)")
200
+ login_p.add_argument("--project", "-p", default=None, help="Default project name")
201
+ login_p.add_argument("--base-url", default="https://api.getcortexops.com")
202
+
203
+ sub.add_parser("logout", help="Remove stored credentials")
204
+ whoami_p = sub.add_parser("whoami", help="Show current credentials and verify key")
205
+ whoami_p.add_argument("--base-url", default=None)
206
+
172
207
  # ── version ───────────────────────────────────────────────────────────
173
208
  sub.add_parser("version", help="Print version and exit")
174
209
 
175
210
  args = parser.parse_args()
176
211
 
177
212
  handlers = {
213
+ ("login", None): cmd_login,
214
+ ("logout", None): cmd_logout,
215
+ ("whoami", None): cmd_whoami,
178
216
  ("eval", "run"): cmd_eval_run,
179
217
  ("eval", "diff"): cmd_eval_diff,
180
218
  ("failures", None): cmd_failures,
@@ -192,4 +230,4 @@ def main() -> None:
192
230
 
193
231
 
194
232
  if __name__ == "__main__":
195
- main()
233
+ main()
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
4
3
  from urllib.parse import urljoin
5
4
 
6
5
  from .models import EvalSummary, Trace
@@ -22,7 +22,6 @@ from typing import Any
22
22
  from .metrics import Metric
23
23
  from .models import EvalCase, FailureKind, Trace
24
24
 
25
-
26
25
  JUDGE_SYSTEM_PROMPT = """You are a strict but fair evaluator of AI agent outputs.
27
26
  You will be given:
28
27
  - The user's input to the agent
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from abc import ABC, abstractmethod
5
- from typing import Any
6
5
 
7
6
  from .models import CaseResult, EvalCase, FailureKind, Trace
8
7
 
@@ -139,7 +138,7 @@ class HallucinationMetric(Metric):
139
138
  return 100.0, None, None
140
139
 
141
140
 
142
- def compute_case_result(case: EvalCase, trace: Trace, extra_metrics: "list[Metric] | None" = None) -> CaseResult:
141
+ def compute_case_result(case: EvalCase, trace: Trace, extra_metrics: list[Metric] | None = None) -> CaseResult:
143
142
  metrics: list[Metric] = [
144
143
  TaskCompletionMetric(),
145
144
  ToolAccuracyMetric(),
@@ -139,3 +139,6 @@ class EvalSummary(BaseModel):
139
139
  for r in failing:
140
140
  lines.append(f" - {r.case_id}: {r.failure_kind or 'unknown'} (score {r.score:.0f})")
141
141
  return "\n".join(lines)
142
+
143
+ def __str__(self) -> str:
144
+ return self.summary()
@@ -1,21 +1,74 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import time
4
5
  import uuid
6
+ from collections.abc import Callable
5
7
  from contextlib import contextmanager
6
- from typing import Any, Callable
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .models import FailureKind, RunStatus, ToolCall, ToolCallStatus, Trace, TraceNode
12
+
13
+ # ── Key resolution order ───────────────────────────────────────────────────
14
+ # 1. Explicit api_key argument
15
+ # 2. CORTEXOPS_API_KEY environment variable
16
+ # 3. ~/.cortexops/credentials file (written by `cortexops login`)
17
+ # 4. None → local-only mode, no hosted tracing
18
+
19
+ _CREDENTIALS_FILE = Path.home() / ".cortexops" / "credentials"
20
+ _DEFAULT_API_URL = "https://api.getcortexops.com"
21
+ _ENV_KEY = "CORTEXOPS_API_KEY"
22
+ _ENV_URL = "CORTEXOPS_API_URL"
23
+ _ENV_PROJECT = "CORTEXOPS_PROJECT"
24
+ _ENV_ENV = "CORTEXOPS_ENVIRONMENT"
25
+
26
+
27
+ def _resolve_api_key(explicit: str | None) -> str | None:
28
+ """Resolve API key from multiple sources in priority order."""
29
+ if explicit:
30
+ return explicit
31
+ # Environment variable
32
+ if env_key := os.getenv(_ENV_KEY):
33
+ return env_key
34
+ # Credentials file written by `cortexops login`
35
+ if _CREDENTIALS_FILE.exists():
36
+ try:
37
+ import json
38
+ creds = json.loads(_CREDENTIALS_FILE.read_text())
39
+ return creds.get("api_key")
40
+ except Exception:
41
+ pass
42
+ return None
7
43
 
8
- from .models import FailureKind, RunStatus, Trace, TraceNode, ToolCall, ToolCallStatus
44
+
45
+ def _resolve_api_url(explicit: str) -> str:
46
+ """Resolve API URL — explicit arg > env var > default."""
47
+ if explicit != _DEFAULT_API_URL:
48
+ return explicit.rstrip("/")
49
+ return os.getenv(_ENV_URL, _DEFAULT_API_URL).rstrip("/")
9
50
 
10
51
 
11
52
  class CortexTracer:
12
53
  """Instruments AI agents with zero-refactor tracing.
13
54
 
14
- Supports LangGraph StateGraph and CrewAI Crew out of the box.
15
- Falls back to a generic callable wrapper for any other agent type.
55
+ API key resolution order (most to least specific):
56
+ 1. api_key argument
57
+ 2. CORTEXOPS_API_KEY environment variable
58
+ 3. ~/.cortexops/credentials (written by `cortexops login`)
59
+ 4. None — local-only mode, traces stored in memory only
16
60
 
17
61
  Usage:
62
+ # Explicit key
63
+ tracer = CortexTracer(project="payments-agent", api_key="cxo-...")
64
+
65
+ # From environment variable (recommended for CI)
66
+ # export CORTEXOPS_API_KEY=cxo-...
18
67
  tracer = CortexTracer(project="payments-agent")
68
+
69
+ # After `cortexops login` (recommended for local dev)
70
+ tracer = CortexTracer(project="payments-agent")
71
+
19
72
  graph = tracer.wrap(your_langgraph_app)
20
73
  result = graph.invoke({"messages": [...]})
21
74
  trace = tracer.last_trace()
@@ -23,30 +76,52 @@ class CortexTracer:
23
76
 
24
77
  def __init__(
25
78
  self,
26
- project: str,
79
+ project: str | None = None,
27
80
  api_key: str | None = None,
28
- environment: str = "development",
81
+ api_url: str = _DEFAULT_API_URL,
82
+ environment: str | None = None,
29
83
  sample_rate: float = 1.0,
30
84
  local_store: bool = True,
31
85
  ) -> None:
32
- self.project = project
33
- self.api_key = api_key
34
- self.environment = environment
86
+ # Project: arg > env var
87
+ self.project = project or os.getenv(_ENV_PROJECT) or "default"
88
+ # Key: auto-resolved from all sources
89
+ self.api_key = _resolve_api_key(api_key)
90
+ self.api_url = _resolve_api_url(api_url)
91
+ # Environment: arg > env var > "development"
92
+ self.environment = environment or os.getenv(_ENV_ENV, "development")
35
93
  self.sample_rate = sample_rate
36
94
  self.local_store = local_store
37
95
  self._traces: list[Trace] = []
38
96
  self._current_trace: Trace | None = None
39
97
 
98
+ # Inform user where key came from — only in development
99
+ if self.environment == "development" and self.api_key:
100
+ source = "argument"
101
+ if not api_key:
102
+ if os.getenv(_ENV_KEY):
103
+ source = f"env:{_ENV_KEY}"
104
+ elif _CREDENTIALS_FILE.exists():
105
+ source = "~/.cortexops/credentials"
106
+ if source != "argument":
107
+ import logging
108
+ logging.getLogger(__name__).debug(
109
+ "CortexTracer: api_key loaded from %s", source
110
+ )
111
+
112
+ @property
113
+ def is_hosted(self) -> bool:
114
+ """True if traces will be shipped to the hosted API."""
115
+ return bool(self.api_key)
116
+
40
117
  def wrap(self, agent: Any) -> Any:
41
118
  """Auto-detect agent type and return an instrumented wrapper."""
42
119
  agent_type = type(agent).__name__
43
120
 
44
121
  if agent_type == "CompiledStateGraph":
45
122
  return self._wrap_langgraph(agent)
46
-
47
123
  if agent_type == "Crew":
48
124
  return self._wrap_crewai(agent)
49
-
50
125
  if callable(agent) or hasattr(agent, "invoke"):
51
126
  return self._wrap_callable(agent)
52
127
 
@@ -62,8 +137,7 @@ class CortexTracer:
62
137
  def invoke(self_, input: dict, config: dict | None = None, **kwargs) -> dict:
63
138
  return tracer._run_traced(
64
139
  fn=lambda: graph.invoke(input, config, **kwargs),
65
- input=input,
66
- framework="langgraph",
140
+ input=input, framework="langgraph",
67
141
  )
68
142
 
69
143
  async def ainvoke(self_, input: dict, config: dict | None = None, **kwargs) -> dict:
@@ -71,8 +145,7 @@ class CortexTracer:
71
145
  return await asyncio.get_event_loop().run_in_executor(
72
146
  None, lambda: tracer._run_traced(
73
147
  fn=lambda: graph.invoke(input, config, **kwargs),
74
- input=input,
75
- framework="langgraph",
148
+ input=input, framework="langgraph",
76
149
  )
77
150
  )
78
151
 
@@ -91,8 +164,7 @@ class CortexTracer:
91
164
  def kickoff(self_, inputs: dict | None = None) -> Any:
92
165
  return tracer._run_traced(
93
166
  fn=lambda: crew.kickoff(inputs=inputs),
94
- input=inputs or {},
95
- framework="crewai",
167
+ input=inputs or {}, framework="crewai",
96
168
  )
97
169
 
98
170
  def __getattr__(self_, name: str):
@@ -104,7 +176,6 @@ class CortexTracer:
104
176
  tracer = self
105
177
 
106
178
  if hasattr(fn, "invoke"):
107
- # Object with .invoke() — wrap that method
108
179
  original_invoke = fn.invoke
109
180
 
110
181
  class InvokeWrapper:
@@ -121,7 +192,6 @@ class CortexTracer:
121
192
 
122
193
  return InvokeWrapper()
123
194
 
124
- # Plain callable
125
195
  def wrapper(*args, **kwargs):
126
196
  input_data = {"args": list(args), "kwargs": kwargs}
127
197
  return tracer._run_traced(fn=lambda: fn(*args, **kwargs), input=input_data, framework="generic")
@@ -129,10 +199,11 @@ class CortexTracer:
129
199
  return wrapper
130
200
 
131
201
  def _run_traced(self, fn: Callable, input: dict, framework: str) -> Any:
132
- trace = Trace(
133
- project=self.project,
134
- input=input,
135
- )
202
+ import random
203
+ if self.sample_rate < 1.0 and random.random() > self.sample_rate:
204
+ return fn()
205
+
206
+ trace = Trace(project=self.project, input=input)
136
207
  self._current_trace = trace
137
208
  t0 = time.perf_counter()
138
209
 
@@ -176,12 +247,9 @@ class CortexTracer:
176
247
  ) -> ToolCall:
177
248
  """Manually record a tool call onto the current active trace."""
178
249
  tc = ToolCall(
179
- name=name,
180
- args=args or {},
181
- result=result,
250
+ name=name, args=args or {}, result=result,
182
251
  status=ToolCallStatus.ERROR if error else ToolCallStatus.SUCCESS,
183
- latency_ms=latency_ms,
184
- error=error,
252
+ latency_ms=latency_ms, error=error,
185
253
  )
186
254
  if self._current_trace and self._current_trace.nodes:
187
255
  self._current_trace.nodes[-1].tool_calls.append(tc)
@@ -201,10 +269,10 @@ class CortexTracer:
201
269
  try:
202
270
  import httpx
203
271
  httpx.post(
204
- "https://api.cortexops.ai/v1/traces",
272
+ f"{self.api_url}/v1/traces",
205
273
  json=trace.model_dump(mode="json"),
206
- headers={"Authorization": f"Bearer {self.api_key}"},
274
+ headers={"X-API-Key": self.api_key},
207
275
  timeout=2.0,
208
276
  )
209
277
  except Exception:
210
- pass # non-blocking — tracing never breaks the agent
278
+ pass # non-blocking — tracing never breaks the agent
@@ -4,19 +4,19 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cortexops"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Reliability infrastructure for AI agents — evaluation, observability, and regression testing"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
11
  authors = [
12
- { name = "Ashish", email = "ashishodu2023@gmail.com" },
12
+ { name = "Ashish", email = "ashish@getcortexops.com" },
13
13
  ]
14
14
  keywords = [
15
15
  "llm", "agents", "evaluation", "observability",
16
16
  "langgraph", "crewai", "autogen", "ai", "testing",
17
17
  ]
18
18
  classifiers = [
19
- "Development Status :: 3 - Alpha",
19
+ "Development Status :: 4 - Beta",
20
20
  "Intended Audience :: Developers",
21
21
  "License :: OSI Approved :: MIT License",
22
22
  "Operating System :: OS Independent",
@@ -33,7 +33,6 @@ requires-python = ">=3.10"
33
33
  dependencies = [
34
34
  "pydantic>=2.0",
35
35
  "pyyaml>=6.0",
36
- "setuptools>=82.0.1",
37
36
  ]
38
37
 
39
38
  [project.optional-dependencies]
@@ -49,9 +48,9 @@ dev = [
49
48
  ]
50
49
 
51
50
  [project.urls]
52
- Homepage = "https://cortexops.ai"
51
+ Homepage = "https://getcortexops.com"
53
52
  Repository = "https://github.com/ashishodu2023/cortexops"
54
- Documentation = "https://docs.cortexops.ai"
53
+ Documentation = "https://docs.getcortexops.com"
55
54
  "Bug Tracker" = "https://github.com/ashishodu2023/cortexops/issues"
56
55
  Changelog = "https://github.com/ashishodu2023/cortexops/releases"
57
56
 
@@ -71,7 +70,7 @@ include = [
71
70
  ]
72
71
 
73
72
  [tool.ruff]
74
- line-length = 100
73
+ line-length = 121
75
74
  target-version = "py310"
76
75
 
77
76
  [tool.ruff.lint]
@@ -85,4 +84,4 @@ ignore_missing_imports = true
85
84
 
86
85
  [tool.pytest.ini_options]
87
86
  asyncio_mode = "auto"
88
- testpaths = ["tests"]
87
+ testpaths = ["tests"]