Skip to content

Commit e4d5956

Browse files
committed
feat(jql): add direct trace scope
1 parent 0940e6f commit e4d5956

4 files changed

Lines changed: 87 additions & 12 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,14 @@ from judgeval import Judgeval
8888
from judgeval.jql import spans
8989

9090
client = Judgeval(project_name="my-project")
91-
result = client.query(spans().rows(), session_ids=["session-123"])
91+
result = client.query(spans().rows(), trace_ids=["trace-123"])
9292
```
9393

94-
`session_ids` is outside the JQL query object. Judgment resolves the sessions
94+
`trace_ids` and `session_ids` are mutually exclusive options outside the JQL
95+
query object. Trace IDs narrow the query directly. Judgment resolves session IDs
9596
within the authenticated organization and project, then narrows every part of
96-
the query to their traces. If none resolve, the request fails instead of
97-
falling back to the whole project. The same option works with `present()` and
97+
the query to their traces. If no session resolves, the request fails instead of
98+
falling back to the whole project. Both options work with `present()` and
9899
`discover()`.
99100

100101
## Integrations

scripts/jql_contract/public-openapi.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,18 @@
278278
"maximum": 10000,
279279
"type": "integer"
280280
},
281+
"trace_ids": {
282+
"minItems": 1,
283+
"maxItems": 1000,
284+
"description": "Traces to bind directly to the query within the authenticated organization and project.",
285+
"type": "array",
286+
"items": {
287+
"minLength": 1,
288+
"maxLength": 512,
289+
"pattern": ".*\\S.*",
290+
"type": "string"
291+
}
292+
},
281293
"session_ids": {
282294
"minItems": 1,
283295
"maxItems": 1000,

src/judgeval/judgeval.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,42 +196,51 @@ def query(
196196
query: "QueryInput",
197197
*,
198198
limit: Optional[int] = None,
199+
trace_ids: Optional[Sequence[str]] = None,
199200
session_ids: Optional[Sequence[str]] = None,
200201
) -> "JqlQueryResponse":
201-
"""Run JQL for this project, optionally narrowed by session IDs."""
202+
"""Run JQL for this project, optionally narrowed by trace or session IDs."""
202203
from judgeval.jql import to_json
203204

204205
return cast(
205206
"JqlQueryResponse",
206-
self._run_jql("query", to_json(query), limit, session_ids),
207+
self._run_jql("query", to_json(query), limit, trace_ids, session_ids),
207208
)
208209

209210
def present(
210211
self,
211212
query: "QueryInput",
212213
*,
213214
limit: Optional[int] = None,
215+
trace_ids: Optional[Sequence[str]] = None,
214216
session_ids: Optional[Sequence[str]] = None,
215217
) -> "JqlPresentationResponse":
216-
"""Run a chart or table JQL query, optionally narrowed by session."""
218+
"""Run a chart or table JQL query, optionally narrowed by trace or session IDs."""
217219
from judgeval.jql import to_json
218220

219221
return cast(
220222
"JqlPresentationResponse",
221-
self._run_jql("query/presentation", to_json(query), limit, session_ids),
223+
self._run_jql(
224+
"query/presentation", to_json(query), limit, trace_ids, session_ids
225+
),
222226
)
223227

224228
def _run_jql(
225229
self,
226230
path: str,
227231
query: Dict[str, Any],
228232
limit: Optional[int],
233+
trace_ids: Optional[Sequence[str]],
229234
session_ids: Optional[Sequence[str]],
230235
) -> Any:
236+
if trace_ids is not None and session_ids is not None:
237+
raise ValueError("trace_ids and session_ids are mutually exclusive")
231238
project_id = self._require_jql_project_id()
232239
payload: Dict[str, Any] = {"query": query}
233240
if limit is not None:
234241
payload["limit"] = limit
242+
if trace_ids is not None:
243+
payload["trace_ids"] = list(trace_ids)
235244
if session_ids is not None:
236245
payload["session_ids"] = list(session_ids)
237246
try:
@@ -251,14 +260,18 @@ def discover(
251260
kind: "DiscoveryKind",
252261
*,
253262
limit: Optional[int] = None,
263+
trace_ids: Optional[Sequence[str]] = None,
254264
session_ids: Optional[Sequence[str]] = None,
255265
**options: Any,
256266
) -> "JqlQueryResponse":
257267
"""Discover project-scoped judges, fields, models, and related values."""
258268
from judgeval.jql import discovery
259269

260270
return self.query(
261-
discovery(kind, **options), limit=limit, session_ids=session_ids
271+
discovery(kind, **options),
272+
limit=limit,
273+
trace_ids=trace_ids,
274+
session_ids=session_ids,
262275
)
263276

264277
def _require_jql_project_id(self) -> str:

src/tests/jql/test_session_scope.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77
from judgeval.jql import spans
88

99

10-
def test_judgeval_query_sends_session_scope_outside_jql(
10+
@pytest.mark.parametrize(
11+
("scope", "expected_scope"),
12+
[
13+
({"trace_ids": ["trace-1"]}, {"trace_ids": ["trace-1"]}),
14+
({"session_ids": ["session-1"]}, {"session_ids": ["session-1"]}),
15+
],
16+
)
17+
def test_judgeval_query_sends_scope_outside_jql(
1118
monkeypatch: pytest.MonkeyPatch,
19+
scope: dict[str, list[str]],
20+
expected_scope: dict[str, list[str]],
1221
) -> None:
1322
monkeypatch.setattr("judgeval.judgeval.resolve_project_id", lambda *_: "project-1")
1423
calls = []
@@ -30,7 +39,7 @@ def fake_request(self, method, url, payload, params=None): # type: ignore[no-un
3039
api_url="https://api.example.com/",
3140
)
3241

33-
response = client.query(spans().rows(), session_ids=["session-1"])
42+
response = client.query(spans().rows(), **scope) # type: ignore[arg-type]
3443

3544
assert {"calls": calls, "response": response} == {
3645
"calls": [
@@ -43,7 +52,7 @@ def fake_request(self, method, url, payload, params=None): # type: ignore[no-un
4352
"source": "spans",
4453
"select": {"op": "rows"},
4554
},
46-
"session_ids": ["session-1"],
55+
**expected_scope,
4756
},
4857
None,
4958
)
@@ -55,3 +64,43 @@ def fake_request(self, method, url, payload, params=None): # type: ignore[no-un
5564
"elapsed_ms": 4,
5665
},
5766
}
67+
68+
69+
def test_judgeval_query_rejects_trace_and_session_scope_together(
70+
monkeypatch: pytest.MonkeyPatch,
71+
) -> None:
72+
monkeypatch.setattr("judgeval.judgeval.resolve_project_id", lambda *_: "project-1")
73+
calls = []
74+
75+
def fake_request(self, method, url, payload, params=None): # type: ignore[no-untyped-def]
76+
calls.append((method, url, payload, params))
77+
raise AssertionError("must not run")
78+
79+
monkeypatch.setattr(JudgmentSyncClient, "_request", fake_request)
80+
client = Judgeval(
81+
project_name="demo",
82+
api_key="api-key",
83+
organization_id="org-1",
84+
api_url="https://api.example.com/",
85+
)
86+
87+
with pytest.raises(ValueError) as caught:
88+
client.query(
89+
spans().rows(),
90+
trace_ids=["trace-1"],
91+
session_ids=["session-1"],
92+
)
93+
94+
assert {
95+
"error": {
96+
"type": type(caught.value).__name__,
97+
"message": str(caught.value),
98+
},
99+
"calls": calls,
100+
} == {
101+
"error": {
102+
"type": "ValueError",
103+
"message": "trace_ids and session_ids are mutually exclusive",
104+
},
105+
"calls": [],
106+
}

0 commit comments

Comments
 (0)