cortexops 0.1.0__tar.gz → 0.3.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 (33) hide show
  1. {cortexops-0.1.0 → cortexops-0.3.0}/.gitignore +33 -33
  2. {cortexops-0.1.0 → cortexops-0.3.0}/PKG-INFO +5 -6
  3. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/__init__.py +9 -3
  4. cortexops-0.3.0/cortexops/auth.py +149 -0
  5. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/cli.py +40 -2
  6. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/client.py +0 -1
  7. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/judge.py +0 -1
  8. {cortexops-0.1.0/cortexops → cortexops-0.3.0}/cortexops/metrics.py +1 -2
  9. {cortexops-0.1.0/cortexops → cortexops-0.3.0}/cortexops/models.py +3 -0
  10. cortexops-0.3.0/cortexops/tracer.py +696 -0
  11. {cortexops-0.1.0 → cortexops-0.3.0}/pyproject.toml +7 -8
  12. cortexops-0.3.0/tests/conftest.py +8 -0
  13. {cortexops-0.1.0/cortexops → cortexops-0.3.0}/tests/test_enhancements.py +178 -2
  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/cortexops/tracer.py +0 -210
  19. cortexops-0.1.0/cortexops/eval.py +0 -216
  20. cortexops-0.1.0/cortexops/metrics.py +0 -184
  21. cortexops-0.1.0/cortexops/models.py +0 -141
  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/tests/test_enhancements.py +0 -222
  26. {cortexops-0.1.0 → cortexops-0.3.0}/LICENSE +0 -0
  27. {cortexops-0.1.0 → cortexops-0.3.0}/README.md +0 -0
  28. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/LICENSE +0 -0
  29. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/README.md +0 -0
  30. {cortexops-0.1.0/cortexops → cortexops-0.3.0}/cortexops/eval.py +0 -0
  31. {cortexops-0.1.0 → cortexops-0.3.0}/cortexops/pyproject.toml +0 -0
  32. {cortexops-0.1.0/cortexops → cortexops-0.3.0}/tests/__init__.py +0 -0
  33. {cortexops-0.1.0/cortexops → cortexops-0.3.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.3.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
@@ -10,6 +10,7 @@ Quickstart:
10
10
  print(results.summary())
11
11
  """
12
12
 
13
+ from .auth import cmd_login, cmd_logout, cmd_whoami, load_credentials, save_credentials
13
14
  from .client import CortexClient
14
15
  from .eval import EvalSuite, EvalThresholdError
15
16
  from .judge import LLMJudgeMetric
@@ -27,13 +28,13 @@ from .models import (
27
28
  EvalSummary,
28
29
  FailureKind,
29
30
  RunStatus,
31
+ ToolCall,
30
32
  Trace,
31
33
  TraceNode,
32
- ToolCall,
33
34
  )
34
35
  from .tracer import CortexTracer
35
36
 
36
- __version__ = "0.1.0"
37
+ __version__ = "0.3.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()