cortexops 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.
cortexops/tracer.py ADDED
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from contextlib import contextmanager
6
+ from typing import Any, Callable
7
+
8
+ from .models import FailureKind, RunStatus, Trace, TraceNode, ToolCall, ToolCallStatus
9
+
10
+
11
+ class CortexTracer:
12
+ """Instruments AI agents with zero-refactor tracing.
13
+
14
+ Supports LangGraph StateGraph and CrewAI Crew out of the box.
15
+ Falls back to a generic callable wrapper for any other agent type.
16
+
17
+ Usage:
18
+ tracer = CortexTracer(project="payments-agent")
19
+ graph = tracer.wrap(your_langgraph_app)
20
+ result = graph.invoke({"messages": [...]})
21
+ trace = tracer.last_trace()
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ project: str,
27
+ api_key: str | None = None,
28
+ environment: str = "development",
29
+ sample_rate: float = 1.0,
30
+ local_store: bool = True,
31
+ ) -> None:
32
+ self.project = project
33
+ self.api_key = api_key
34
+ self.environment = environment
35
+ self.sample_rate = sample_rate
36
+ self.local_store = local_store
37
+ self._traces: list[Trace] = []
38
+ self._current_trace: Trace | None = None
39
+
40
+ def wrap(self, agent: Any) -> Any:
41
+ """Auto-detect agent type and return an instrumented wrapper."""
42
+ agent_type = type(agent).__name__
43
+
44
+ if agent_type == "CompiledStateGraph":
45
+ return self._wrap_langgraph(agent)
46
+
47
+ if agent_type == "Crew":
48
+ return self._wrap_crewai(agent)
49
+
50
+ if callable(agent) or hasattr(agent, "invoke"):
51
+ return self._wrap_callable(agent)
52
+
53
+ raise TypeError(
54
+ f"CortexTracer.wrap() does not support {agent_type}. "
55
+ "Pass a LangGraph CompiledStateGraph, CrewAI Crew, or any callable."
56
+ )
57
+
58
+ def _wrap_langgraph(self, graph: Any) -> Any:
59
+ tracer = self
60
+
61
+ class InstrumentedGraph:
62
+ def invoke(self_, input: dict, config: dict | None = None, **kwargs) -> dict:
63
+ return tracer._run_traced(
64
+ fn=lambda: graph.invoke(input, config, **kwargs),
65
+ input=input,
66
+ framework="langgraph",
67
+ )
68
+
69
+ async def ainvoke(self_, input: dict, config: dict | None = None, **kwargs) -> dict:
70
+ import asyncio
71
+ return await asyncio.get_event_loop().run_in_executor(
72
+ None, lambda: tracer._run_traced(
73
+ fn=lambda: graph.invoke(input, config, **kwargs),
74
+ input=input,
75
+ framework="langgraph",
76
+ )
77
+ )
78
+
79
+ def stream(self_, input: dict, config: dict | None = None, **kwargs):
80
+ return graph.stream(input, config, **kwargs)
81
+
82
+ def __getattr__(self_, name: str):
83
+ return getattr(graph, name)
84
+
85
+ return InstrumentedGraph()
86
+
87
+ def _wrap_crewai(self, crew: Any) -> Any:
88
+ tracer = self
89
+
90
+ class InstrumentedCrew:
91
+ def kickoff(self_, inputs: dict | None = None) -> Any:
92
+ return tracer._run_traced(
93
+ fn=lambda: crew.kickoff(inputs=inputs),
94
+ input=inputs or {},
95
+ framework="crewai",
96
+ )
97
+
98
+ def __getattr__(self_, name: str):
99
+ return getattr(crew, name)
100
+
101
+ return InstrumentedCrew()
102
+
103
+ def _wrap_callable(self, fn: Any) -> Any:
104
+ tracer = self
105
+
106
+ if hasattr(fn, "invoke"):
107
+ # Object with .invoke() — wrap that method
108
+ original_invoke = fn.invoke
109
+
110
+ class InvokeWrapper:
111
+ def invoke(self_, *args, **kwargs):
112
+ input_data = args[0] if args else kwargs
113
+ return tracer._run_traced(
114
+ fn=lambda: original_invoke(*args, **kwargs),
115
+ input=input_data if isinstance(input_data, dict) else {"input": input_data},
116
+ framework="generic",
117
+ )
118
+
119
+ def __getattr__(self_, name: str):
120
+ return getattr(fn, name)
121
+
122
+ return InvokeWrapper()
123
+
124
+ # Plain callable
125
+ def wrapper(*args, **kwargs):
126
+ input_data = {"args": list(args), "kwargs": kwargs}
127
+ return tracer._run_traced(fn=lambda: fn(*args, **kwargs), input=input_data, framework="generic")
128
+
129
+ return wrapper
130
+
131
+ def _run_traced(self, fn: Callable, input: dict, framework: str) -> Any:
132
+ trace = Trace(
133
+ project=self.project,
134
+ input=input,
135
+ )
136
+ self._current_trace = trace
137
+ t0 = time.perf_counter()
138
+
139
+ try:
140
+ result = fn()
141
+ trace.total_latency_ms = (time.perf_counter() - t0) * 1000
142
+ trace.status = RunStatus.COMPLETED
143
+ trace.output = result if isinstance(result, dict) else {"result": str(result)}
144
+ except Exception as exc:
145
+ trace.total_latency_ms = (time.perf_counter() - t0) * 1000
146
+ trace.status = RunStatus.FAILED
147
+ trace.failure_kind = FailureKind.UNKNOWN
148
+ trace.failure_detail = str(exc)
149
+ raise
150
+ finally:
151
+ self._traces.append(trace)
152
+ if self.api_key:
153
+ self._flush_trace(trace)
154
+
155
+ return result
156
+
157
+ @contextmanager
158
+ def trace_node(self, node_name: str):
159
+ """Context manager to manually instrument a single node."""
160
+ node = TraceNode(node_id=str(uuid.uuid4()), node_name=node_name)
161
+ t0 = time.perf_counter()
162
+ try:
163
+ yield node
164
+ finally:
165
+ node.latency_ms = (time.perf_counter() - t0) * 1000
166
+ if self._current_trace:
167
+ self._current_trace.nodes.append(node)
168
+
169
+ def record_tool_call(
170
+ self,
171
+ name: str,
172
+ args: dict | None = None,
173
+ result: Any = None,
174
+ error: str | None = None,
175
+ latency_ms: float = 0.0,
176
+ ) -> ToolCall:
177
+ """Manually record a tool call onto the current active trace."""
178
+ tc = ToolCall(
179
+ name=name,
180
+ args=args or {},
181
+ result=result,
182
+ status=ToolCallStatus.ERROR if error else ToolCallStatus.SUCCESS,
183
+ latency_ms=latency_ms,
184
+ error=error,
185
+ )
186
+ if self._current_trace and self._current_trace.nodes:
187
+ self._current_trace.nodes[-1].tool_calls.append(tc)
188
+ return tc
189
+
190
+ def last_trace(self) -> Trace | None:
191
+ return self._traces[-1] if self._traces else None
192
+
193
+ def traces(self) -> list[Trace]:
194
+ return list(self._traces)
195
+
196
+ def clear(self) -> None:
197
+ self._traces.clear()
198
+ self._current_trace = None
199
+
200
+ def _flush_trace(self, trace: Trace) -> None:
201
+ try:
202
+ import httpx
203
+ httpx.post(
204
+ "https://api.cortexops.ai/v1/traces",
205
+ json=trace.model_dump(mode="json"),
206
+ headers={"Authorization": f"Bearer {self.api_key}"},
207
+ timeout=2.0,
208
+ )
209
+ except Exception:
210
+ pass # non-blocking — tracing never breaks the agent
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortexops
3
+ Version: 0.1.0
4
+ Summary: Reliability infrastructure for AI agents — evaluation, observability, and regression testing
5
+ Project-URL: Homepage, https://cortexops.ai
6
+ Project-URL: Repository, https://github.com/ashishodu2023/cortexops
7
+ Project-URL: Documentation, https://docs.cortexops.ai
8
+ Project-URL: Bug Tracker, https://github.com/ashishodu2023/cortexops/issues
9
+ Project-URL: Changelog, https://github.com/ashishodu2023/cortexops/releases
10
+ Author-email: Ashish <ashishodu2023@gmail.com>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2025 CortexOps Contributors
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: agents,ai,autogen,crewai,evaluation,langgraph,llm,observability,testing
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
43
+ Classifier: Topic :: Software Development :: Quality Assurance
44
+ Classifier: Topic :: Software Development :: Testing
45
+ Classifier: Typing :: Typed
46
+ Requires-Python: >=3.10
47
+ Requires-Dist: pydantic>=2.0
48
+ Requires-Dist: pyyaml>=6.0
49
+ Requires-Dist: setuptools>=82.0.1
50
+ Provides-Extra: all
51
+ Requires-Dist: httpx>=0.27; extra == 'all'
52
+ Provides-Extra: dev
53
+ Requires-Dist: httpx>=0.27; extra == 'dev'
54
+ Requires-Dist: mypy>=1.10; extra == 'dev'
55
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
56
+ Requires-Dist: pytest>=8.0; extra == 'dev'
57
+ Requires-Dist: ruff>=0.4; extra == 'dev'
58
+ Provides-Extra: http
59
+ Requires-Dist: httpx>=0.27; extra == 'http'
60
+ Provides-Extra: llm
61
+ Requires-Dist: httpx>=0.27; extra == 'llm'
62
+ Description-Content-Type: text/markdown
63
+
64
+ # CortexOps
65
+
66
+ **Reliability infrastructure for AI agents.**
67
+ Evaluate · Observe · Operate — for LangGraph, CrewAI, and AutoGen.
68
+
69
+ [![PyPI version](https://img.shields.io/pypi/v/cortexops.svg)](https://pypi.org/project/cortexops/)
70
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
71
+ [![CI](https://github.com/ashishodu2023/cortexops/actions/workflows/eval.yml/badge.svg)](https://github.com/ashishodu2023/cortexops/actions/workflows/eval.yml)
72
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/ashishodu2023/cortexops/blob/main/LICENSE)
73
+
74
+ ---
75
+
76
+ ## The problem
77
+
78
+ You deployed an agent. You have no idea if it regressed overnight.
79
+
80
+ No standard eval format. No failure traces. No CI gate before the next prompt change ships.
81
+ CortexOps fixes that.
82
+
83
+ ---
84
+
85
+ ## Install
86
+
87
+ ```bash
88
+ pip install cortexops
89
+
90
+ # With HTTP client (for pushing traces to hosted API):
91
+ pip install cortexops[http]
92
+
93
+ # With LLM judge support:
94
+ pip install cortexops[llm]
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Quickstart
100
+
101
+ ```python
102
+ from cortexops import CortexTracer, EvalSuite
103
+
104
+ # Wrap your LangGraph app — zero refactor required
105
+ tracer = CortexTracer(project="payments-agent")
106
+ graph = tracer.wrap(your_langgraph_app)
107
+
108
+ # Run evaluations against a golden dataset
109
+ results = EvalSuite.run(
110
+ dataset="golden_v1.yaml",
111
+ agent=graph,
112
+ )
113
+ print(results.summary())
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Golden dataset (YAML)
119
+
120
+ ```yaml
121
+ version: 1
122
+ project: payments-agent
123
+
124
+ cases:
125
+ - id: refund_lookup_01
126
+ input: "What is the status of refund REF-8821?"
127
+ expected_tool_calls: [lookup_refund]
128
+ expected_output_contains: ["approved", "REF-8821"]
129
+ max_latency_ms: 3000
130
+
131
+ - id: open_ended_explanation_01
132
+ input: "Why was my refund rejected?"
133
+ judge: llm
134
+ judge_criteria: >
135
+ The response must explain the rejection reason clearly,
136
+ be empathetic, and offer a concrete next step. No jargon.
137
+ ```
138
+
139
+ ---
140
+
141
+ ## CI gate
142
+
143
+ ```bash
144
+ cortexops eval run \
145
+ --dataset golden_v1.yaml \
146
+ --fail-on "task_completion < 0.90"
147
+ ```
148
+
149
+ Exits non-zero if the threshold is not met — blocks the PR.
150
+
151
+ ---
152
+
153
+ ## Built-in metrics
154
+
155
+ | Metric | What it checks |
156
+ |---|---|
157
+ | `task_completion` | Non-empty, non-error output with expected content |
158
+ | `tool_accuracy` | Expected tool calls were actually made |
159
+ | `latency` | Response within `max_latency_ms` budget |
160
+ | `hallucination` | Fabrication signals in output |
161
+ | `llm_judge` | GPT-4o scores against natural-language criteria |
162
+
163
+ ---
164
+
165
+ ## Links
166
+
167
+ - **Docs**: [docs.cortexops.ai](https://docs.cortexops.ai)
168
+ - **Repo**: [github.com/ashishodu2023/cortexops](https://github.com/ashishodu2023/cortexops)
169
+ - **Issues**: [GitHub Issues](https://github.com/ashishodu2023/cortexops/issues)
@@ -0,0 +1,27 @@
1
+ cortexops/LICENSE,sha256=cXemgb-9EkUWB7EQ1riVRZn2gVQ1JcG7U34LpkUjaZA,1079
2
+ cortexops/README.md,sha256=ydnSnOOqE2BbXgpFiusbaNQORc69ANkhQoeoJZAmHhc,2720
3
+ cortexops/__init__.py,sha256=6IBgalIuUUlXz3TlliO3ERUpi-FJQwFE71CY1HY1C5s,1168
4
+ cortexops/cli.py,sha256=oZKt6Xow6srU3Xm5GJ0-5OsfQAyHphPxzFNJIqHLU0s,7281
5
+ cortexops/client.py,sha256=AE9hhcdlP2D-_QwQN0Qj4572WNqVykYTB0JOi7917R0,2754
6
+ cortexops/eval.py,sha256=BkVoYLDzx15ZhJ0V-whUROYqBvSUgM_3l10L7yQu5yA,7248
7
+ cortexops/judge.py,sha256=ILJNaTySkfqT-dAnafmr48bb9N9YiDXoNo_JmZLQUBM,5386
8
+ cortexops/metrics.py,sha256=BMK8I0ceabpo0yZvP5lVvL9lCPBCZ8yNptidYeXwIK8,6545
9
+ cortexops/models.py,sha256=9mx2ZUAJJyzSQXmTsVqJfLRLorRyuQ_MIhmnHrXYABE,4441
10
+ cortexops/pyproject.toml,sha256=rDbZVYog_dZ7gg7mR_RVNgCMNulvcn70nK7AIIArbl8,2164
11
+ cortexops/tracer.py,sha256=ySoDkUZwkSzWUnk9cs9puf_Z3Wz4HdvGhxbOSbjIlcw,7108
12
+ cortexops/cortexops/__init__.py,sha256=6IBgalIuUUlXz3TlliO3ERUpi-FJQwFE71CY1HY1C5s,1168
13
+ cortexops/cortexops/cli.py,sha256=oZKt6Xow6srU3Xm5GJ0-5OsfQAyHphPxzFNJIqHLU0s,7281
14
+ cortexops/cortexops/client.py,sha256=AE9hhcdlP2D-_QwQN0Qj4572WNqVykYTB0JOi7917R0,2754
15
+ cortexops/cortexops/eval.py,sha256=BkVoYLDzx15ZhJ0V-whUROYqBvSUgM_3l10L7yQu5yA,7248
16
+ cortexops/cortexops/judge.py,sha256=ILJNaTySkfqT-dAnafmr48bb9N9YiDXoNo_JmZLQUBM,5386
17
+ cortexops/cortexops/metrics.py,sha256=BMK8I0ceabpo0yZvP5lVvL9lCPBCZ8yNptidYeXwIK8,6545
18
+ cortexops/cortexops/models.py,sha256=9mx2ZUAJJyzSQXmTsVqJfLRLorRyuQ_MIhmnHrXYABE,4441
19
+ cortexops/cortexops/tracer.py,sha256=ySoDkUZwkSzWUnk9cs9puf_Z3Wz4HdvGhxbOSbjIlcw,7108
20
+ cortexops/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ cortexops/tests/test_cortexops.py,sha256=SVoeR3ZfCca4J0Kg4-HM4_MPmoTGSElVcPC-OQqdYRM,7967
22
+ cortexops/tests/test_enhancements.py,sha256=gqxrwuF9EuDxunLlqNFmwSCoq9gm0BrYN81ZgM3WBLc,8238
23
+ cortexops-0.1.0.dist-info/METADATA,sha256=UCATLqZfsI3Y9RMj99ZjMmS2MvJqUNVswfO0byXpgAo,5783
24
+ cortexops-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
25
+ cortexops-0.1.0.dist-info/entry_points.txt,sha256=wBc4X1RuV2sUDyF5TLPmsRrJQ3GrbewjPeC0K0C6r0k,49
26
+ cortexops-0.1.0.dist-info/licenses/LICENSE,sha256=cXemgb-9EkUWB7EQ1riVRZn2gVQ1JcG7U34LpkUjaZA,1079
27
+ cortexops-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cortexops = cortexops.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CortexOps Contributors
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 DEALINGS IN THE
21
+ SOFTWARE.