ctxprotocol 0.20.0__tar.gz → 0.22.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.
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/CHANGELOG.md +16 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/PKG-INFO +5 -5
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/README.md +4 -4
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/__init__.py +1 -1
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/resources/query.py +26 -26
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/types.py +3 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/pyproject.toml +1 -1
- ctxprotocol-0.22.0/scripts/hermes-parity-sanity-check.py +153 -0
- ctxprotocol-0.22.0/scripts/hermes-parity-stream-check.py +96 -0
- ctxprotocol-0.22.0/scripts/medium-query.txt +1 -0
- ctxprotocol-0.22.0/scripts/saudi-oil-query.txt +16 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/test_query.py +141 -137
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/.codexignore +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/.gitignore +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/auth/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/client.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/resources/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/resources/developer.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/resources/discovery.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/client/resources/tools.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/context/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/context/hyperliquid.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/context/polymarket.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/context/wallet.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/contrib/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/contrib/search/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/contrib/search/core.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/contrib/search/trace.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/contrib/search/types.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/contrib/search/validation.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/handshake/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/ctxprotocol/py.typed +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/docs/README.md +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/execute_client.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/polymarket_query_trace_validation.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/query_client.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/test_favorites_only.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/test_get_tool.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/two_surfaces_client.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/server/hummingbot-contributor/README.md +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/server/hummingbot-contributor/env.example +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/server/hummingbot-contributor/requirements.txt +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/server/hummingbot-contributor/server.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/polymarket-query-trace-results-py.json +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/__init__.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/contrib_search_validation_cases.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/test_client.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/test_contrib_search.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/test_discovery.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/tests/test_tools.py +0 -0
- {ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/uv.lock +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
- Added `bounded_explicit_empty_result_guardrail` to the query controller stop-reason literal type so strict consumers no longer fail validation when the platform returns that live stop reason.
|
|
6
|
+
- Platform note (no type change): `computed_artifacts` is now capped at 4 per response by the shared artifact emission policy, so SDK/MCP consumers receive exactly the artifact list the web app displays for the identical run.
|
|
7
|
+
|
|
8
|
+
## 0.21.0
|
|
9
|
+
|
|
10
|
+
- `client.query.run()` is now backed by the durable job path (`start()` + `poll()`) instead of a held-open SSE connection. One call now reliably covers the full 1800s hosted compute ceiling and survives transient connection drops — the "sometimes works, sometimes times out on hard queries" failure mode is gone. `run()` accepts optional `interval_ms` / `timeout_ms` keyword arguments.
|
|
11
|
+
- `run_or_poll()` is kept as an explicit alias of the same path, and now also synthesizes a fallback developer trace when `include_developer_trace` is set but the backend omits the trace (matching prior `run()` behavior).
|
|
12
|
+
- `client.query.stream()` is unchanged and remains the real-time SSE surface.
|
|
13
|
+
|
|
14
|
+
## 0.20.0
|
|
15
|
+
|
|
16
|
+
- Poll defaults aligned with the hosted 1800s compute ceiling: `poll()`/`run_or_poll()` check status every 5 seconds over plain HTTP and wait up to 31 minutes by default.
|
|
17
|
+
- Documented that HTTP polling costs no model tokens; model turns do.
|
|
18
|
+
|
|
3
19
|
## 0.19.1
|
|
4
20
|
|
|
5
21
|
- Fixed `client.query.start()` returning `400 "Invalid query job request"` when called without explicit `tools`. The request body builder now omits optional fields (notably `tools`) when unset instead of serializing them as JSON `null`; the durable jobs endpoint's Zod schema rejects `null` for optional fields. `client.query.run()` and `client.query.stream()` were not failing (the `/api/v1/query` endpoint tolerates `null`), but are now consistent with the TS SDK and MCP behavior.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxprotocol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.22.0
|
|
4
4
|
Summary: Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically
|
|
5
5
|
Project-URL: Homepage, https://ctxprotocol.com
|
|
6
6
|
Project-URL: Documentation, https://docs.ctxprotocol.com
|
|
@@ -149,7 +149,7 @@ print(
|
|
|
149
149
|
print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
Since 0.21.0 `run()` is backed by the durable job path (`start()` + `poll()`): one call works the same for a 5-second lookup and a 30-minute chart build, with no held-open SSE connection for proxies or client timeouts to kill. `run_or_poll()` remains as an explicit alias:
|
|
153
153
|
|
|
154
154
|
```python
|
|
155
155
|
answer = await client.query.run_or_poll(
|
|
@@ -249,7 +249,7 @@ See a full dual-surface client script in [`examples/two-surfaces-client.py`](./e
|
|
|
249
249
|
| `api_key` | `str` | Yes | — | Your Context Protocol API key |
|
|
250
250
|
| `base_url` | `str` | No | `https://www.ctxprotocol.com` | API base URL (for development) |
|
|
251
251
|
| `request_timeout_seconds` | `float` | No | `300.0` | Timeout for non-streaming JSON API calls |
|
|
252
|
-
| `stream_timeout_seconds` | `float` | No | `600.0` | Timeout for streaming API calls
|
|
252
|
+
| `stream_timeout_seconds` | `float` | No | `600.0` | Timeout for streaming API calls (`client.query.stream()`) |
|
|
253
253
|
|
|
254
254
|
```python
|
|
255
255
|
# Production
|
|
@@ -342,7 +342,7 @@ closed = await client.tools.close_session("sess_123")
|
|
|
342
342
|
|
|
343
343
|
Run an agentic query. The server applies the live librarian pipeline (`discover -> select -> iterative execute -> synthesize -> settle`) with up to 100 MCP calls per response turn, then returns the selected Query response contract (`answer_with_evidence` or `evidence_only`, default `answer_with_evidence`).
|
|
344
344
|
|
|
345
|
-
`client.query.run()`
|
|
345
|
+
Since 0.21.0, `client.query.run()` is backed by the durable job path (`start()` + `poll()`), so a single call reliably covers the full 1800s hosted compute ceiling and survives transient connection drops. This matches the TypeScript SDK. Use `client.query.stream()` when you want real-time SSE events.
|
|
346
346
|
|
|
347
347
|
The query runtime now exposes a single managed executor surface.
|
|
348
348
|
The server decides internal budgets, ambiguity handling, and exploration policy
|
|
@@ -383,7 +383,7 @@ When retrieval-first synthesis rollout is enabled server-side, full-data or trun
|
|
|
383
383
|
|
|
384
384
|
#### `client.query.stream(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
|
|
385
385
|
|
|
386
|
-
|
|
386
|
+
Runs the same query pipeline as `run()` but over a live SSE connection, yielding events in real-time.
|
|
387
387
|
|
|
388
388
|
Event types:
|
|
389
389
|
- `tool-status`
|
|
@@ -111,7 +111,7 @@ print(
|
|
|
111
111
|
print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
Since 0.21.0 `run()` is backed by the durable job path (`start()` + `poll()`): one call works the same for a 5-second lookup and a 30-minute chart build, with no held-open SSE connection for proxies or client timeouts to kill. `run_or_poll()` remains as an explicit alias:
|
|
115
115
|
|
|
116
116
|
```python
|
|
117
117
|
answer = await client.query.run_or_poll(
|
|
@@ -211,7 +211,7 @@ See a full dual-surface client script in [`examples/two-surfaces-client.py`](./e
|
|
|
211
211
|
| `api_key` | `str` | Yes | — | Your Context Protocol API key |
|
|
212
212
|
| `base_url` | `str` | No | `https://www.ctxprotocol.com` | API base URL (for development) |
|
|
213
213
|
| `request_timeout_seconds` | `float` | No | `300.0` | Timeout for non-streaming JSON API calls |
|
|
214
|
-
| `stream_timeout_seconds` | `float` | No | `600.0` | Timeout for streaming API calls
|
|
214
|
+
| `stream_timeout_seconds` | `float` | No | `600.0` | Timeout for streaming API calls (`client.query.stream()`) |
|
|
215
215
|
|
|
216
216
|
```python
|
|
217
217
|
# Production
|
|
@@ -304,7 +304,7 @@ closed = await client.tools.close_session("sess_123")
|
|
|
304
304
|
|
|
305
305
|
Run an agentic query. The server applies the live librarian pipeline (`discover -> select -> iterative execute -> synthesize -> settle`) with up to 100 MCP calls per response turn, then returns the selected Query response contract (`answer_with_evidence` or `evidence_only`, default `answer_with_evidence`).
|
|
306
306
|
|
|
307
|
-
`client.query.run()`
|
|
307
|
+
Since 0.21.0, `client.query.run()` is backed by the durable job path (`start()` + `poll()`), so a single call reliably covers the full 1800s hosted compute ceiling and survives transient connection drops. This matches the TypeScript SDK. Use `client.query.stream()` when you want real-time SSE events.
|
|
308
308
|
|
|
309
309
|
The query runtime now exposes a single managed executor surface.
|
|
310
310
|
The server decides internal budgets, ambiguity handling, and exploration policy
|
|
@@ -345,7 +345,7 @@ When retrieval-first synthesis rollout is enabled server-side, full-data or trun
|
|
|
345
345
|
|
|
346
346
|
#### `client.query.stream(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
Runs the same query pipeline as `run()` but over a live SSE connection, yielding events in real-time.
|
|
349
349
|
|
|
350
350
|
Event types:
|
|
351
351
|
- `tool-status`
|
|
@@ -265,6 +265,8 @@ class Query:
|
|
|
265
265
|
resume_from: QueryAttemptReference | dict[str, Any] | None = None,
|
|
266
266
|
fork_from: QueryForkReference | dict[str, Any] | None = None,
|
|
267
267
|
idempotency_key: str | None = None,
|
|
268
|
+
interval_ms: int = DEFAULT_QUERY_POLL_INTERVAL_MS,
|
|
269
|
+
timeout_ms: int = DEFAULT_QUERY_POLL_TIMEOUT_MS,
|
|
268
270
|
) -> QueryResult:
|
|
269
271
|
"""Run an agentic query and wait for the full response.
|
|
270
272
|
|
|
@@ -272,8 +274,12 @@ class Query:
|
|
|
272
274
|
executes the discovery-first pipeline (up to 100 MCP calls per tool),
|
|
273
275
|
and returns an AI-synthesized answer. Payment is settled after
|
|
274
276
|
successful execution via deferred settlement.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
|
|
278
|
+
Since 0.21.0 this is backed by the durable job path (``start()`` +
|
|
279
|
+
``poll()``), so one call reliably survives the full 1800s hosted
|
|
280
|
+
compute ceiling and transient connection drops — no held-open SSE
|
|
281
|
+
connection to be killed by proxies or client timeouts. Use
|
|
282
|
+
``stream()`` when you want real-time SSE events instead.
|
|
277
283
|
|
|
278
284
|
Args:
|
|
279
285
|
query: The natural-language question to answer
|
|
@@ -288,6 +294,8 @@ class Query:
|
|
|
288
294
|
resume_from: Resume a prior durable query attempt
|
|
289
295
|
fork_from: Fork a new durable query attempt from a previous attempt
|
|
290
296
|
idempotency_key: Optional idempotency key (UUID recommended) for safe retries
|
|
297
|
+
interval_ms: Internal HTTP status-check cadence (costs no LLM tokens)
|
|
298
|
+
timeout_ms: Max client-side wait for the job's terminal state
|
|
291
299
|
|
|
292
300
|
Returns:
|
|
293
301
|
The complete query result with response text, tools used, and cost
|
|
@@ -311,9 +319,7 @@ class Query:
|
|
|
311
319
|
... tools=["tool-uuid-1", "tool-uuid-2"],
|
|
312
320
|
... )
|
|
313
321
|
"""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
async for event in self.stream(
|
|
322
|
+
return await self.run_or_poll(
|
|
317
323
|
query=query,
|
|
318
324
|
tools=tools,
|
|
319
325
|
favorites_only=favorites_only,
|
|
@@ -325,21 +331,9 @@ class Query:
|
|
|
325
331
|
resume_from=resume_from,
|
|
326
332
|
fork_from=fork_from,
|
|
327
333
|
idempotency_key=idempotency_key,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
continue
|
|
332
|
-
|
|
333
|
-
if event.type == "done":
|
|
334
|
-
return event.result
|
|
335
|
-
|
|
336
|
-
if terminal_error is not None:
|
|
337
|
-
raise ContextError(
|
|
338
|
-
message=terminal_error.error,
|
|
339
|
-
code=terminal_error.code,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
raise ContextError("Streaming query ended before done event")
|
|
334
|
+
interval_ms=interval_ms,
|
|
335
|
+
timeout_ms=timeout_ms,
|
|
336
|
+
)
|
|
343
337
|
|
|
344
338
|
async def start(
|
|
345
339
|
self,
|
|
@@ -435,11 +429,11 @@ class Query:
|
|
|
435
429
|
) -> QueryResult:
|
|
436
430
|
"""Run a durable query job and wait internally for completion.
|
|
437
431
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
by the 1800s hosted compute ceiling.
|
|
432
|
+
``run()`` delegates here since 0.21.0, so the two are equivalent;
|
|
433
|
+
``run_or_poll()`` is kept as an explicit alias. The entire wait
|
|
434
|
+
happens inside this single call (one model turn for LLM agents),
|
|
435
|
+
instead of one turn per ``get_status()`` check, and the job is
|
|
436
|
+
bounded by the 1800s hosted compute ceiling.
|
|
443
437
|
"""
|
|
444
438
|
job = await self.start(
|
|
445
439
|
query=query,
|
|
@@ -460,7 +454,13 @@ class Query:
|
|
|
460
454
|
timeout_ms=timeout_ms,
|
|
461
455
|
)
|
|
462
456
|
if completed.status == "completed" and completed.result is not None:
|
|
463
|
-
|
|
457
|
+
result = completed.result
|
|
458
|
+
if include_developer_trace and result.developer_trace is None:
|
|
459
|
+
result.developer_trace = self._build_synthetic_trace_from_run_result(
|
|
460
|
+
result.tools_used,
|
|
461
|
+
result.duration_ms,
|
|
462
|
+
)
|
|
463
|
+
return result
|
|
464
464
|
raise ContextError(completed.error or "Context query job completed without a result")
|
|
465
465
|
|
|
466
466
|
async def stream(
|
|
@@ -47,6 +47,7 @@ DEFAULT_AGENT_MODEL_ID = "kimi-k2.6-model"
|
|
|
47
47
|
AGENT_MODEL_IDS = (
|
|
48
48
|
"kimi-k2.6-model",
|
|
49
49
|
"glm-5.2-model",
|
|
50
|
+
"grok-4.5-model",
|
|
50
51
|
"deepseek-v4-pro-model",
|
|
51
52
|
"deepseek-v4-flash-model",
|
|
52
53
|
"qwen-3.7-plus-model",
|
|
@@ -57,6 +58,7 @@ AGENT_MODEL_IDS = (
|
|
|
57
58
|
AgentModelId = Literal[
|
|
58
59
|
"kimi-k2.6-model",
|
|
59
60
|
"glm-5.2-model",
|
|
61
|
+
"grok-4.5-model",
|
|
60
62
|
"deepseek-v4-pro-model",
|
|
61
63
|
"deepseek-v4-flash-model",
|
|
62
64
|
"qwen-3.7-plus-model",
|
|
@@ -1461,6 +1463,7 @@ QueryControllerStopReason = Literal[
|
|
|
1461
1463
|
"bounded_runtime_budget",
|
|
1462
1464
|
"bounded_same_endpoint_guardrail",
|
|
1463
1465
|
"bounded_upstream_abort_guardrail",
|
|
1466
|
+
"bounded_explicit_empty_result_guardrail",
|
|
1464
1467
|
"capability_miss",
|
|
1465
1468
|
]
|
|
1466
1469
|
QueryControllerIssueClass = Literal[
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Throwaway Hermes-parity sanity check for ctxprotocol 0.21.0.
|
|
3
|
+
|
|
4
|
+
Mirrors ~/.hermes/workspace/ctx-gtm/scripts/ctxprotocol-query.py:
|
|
5
|
+
- same ContextClient timeouts
|
|
6
|
+
- same query.run() kwargs (no agent_model_id)
|
|
7
|
+
- same output contract (JSON + CHART_URL + COST_USD)
|
|
8
|
+
|
|
9
|
+
Reads CONTEXT_API_KEY from context-sdk/.env.local (or env).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
SDK_QUERY_TIMEOUT = 1860 # Hermes: 1800s ceiling + 60s grace
|
|
21
|
+
FAVORITES_ONLY = os.environ.get("CTX_FAVORITES_ONLY", "true").strip().lower() in (
|
|
22
|
+
"true",
|
|
23
|
+
"1",
|
|
24
|
+
"yes",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
DEFAULT_QUERY = (
|
|
28
|
+
"What is the current price of Bitcoin? One sentence answer, no charts needed."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_api_key() -> str:
|
|
33
|
+
candidates = [
|
|
34
|
+
os.environ.get("CONTEXT_API_KEY"),
|
|
35
|
+
_read_env_file(
|
|
36
|
+
os.path.join(
|
|
37
|
+
os.path.dirname(__file__),
|
|
38
|
+
"..",
|
|
39
|
+
"..",
|
|
40
|
+
"context-sdk",
|
|
41
|
+
".env.local",
|
|
42
|
+
)
|
|
43
|
+
),
|
|
44
|
+
_read_env_file(
|
|
45
|
+
os.path.expanduser("~/.hermes/.env"),
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
for value in candidates:
|
|
49
|
+
if value:
|
|
50
|
+
return value
|
|
51
|
+
print(
|
|
52
|
+
"ERROR: CONTEXT_API_KEY not found in env, context-sdk/.env.local, or ~/.hermes/.env",
|
|
53
|
+
file=sys.stderr,
|
|
54
|
+
)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_env_file(path: str) -> str | None:
|
|
59
|
+
try:
|
|
60
|
+
with open(os.path.abspath(path)) as handle:
|
|
61
|
+
for line in handle:
|
|
62
|
+
line = line.strip()
|
|
63
|
+
if line.startswith("CONTEXT_API_KEY="):
|
|
64
|
+
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
65
|
+
except OSError:
|
|
66
|
+
return None
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _chart_urls_from_result(ans) -> list[str]:
|
|
71
|
+
urls: list[str] = []
|
|
72
|
+
for art in getattr(ans, "computed_artifacts", None) or []:
|
|
73
|
+
if getattr(art, "kind", None) == "image":
|
|
74
|
+
url = getattr(art, "url", None)
|
|
75
|
+
if url:
|
|
76
|
+
urls.append(url)
|
|
77
|
+
if not urls:
|
|
78
|
+
for text in (getattr(ans, "response", None), getattr(ans, "summary", None)):
|
|
79
|
+
if not text:
|
|
80
|
+
continue
|
|
81
|
+
for match in re.finditer(r"!\[.*?\]\((https?://[^)\s]+)\)", text):
|
|
82
|
+
urls.append(match.group(1))
|
|
83
|
+
seen: set[str] = set()
|
|
84
|
+
deduped: list[str] = []
|
|
85
|
+
for url in urls:
|
|
86
|
+
if url not in seen:
|
|
87
|
+
seen.add(url)
|
|
88
|
+
deduped.append(url)
|
|
89
|
+
return deduped
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def emit(ans) -> None:
|
|
93
|
+
cost = getattr(ans, "cost", None)
|
|
94
|
+
total = getattr(cost, "total_cost_usd", None) if cost else None
|
|
95
|
+
payload = {
|
|
96
|
+
"response": getattr(ans, "response", None),
|
|
97
|
+
"summary": getattr(ans, "summary", None),
|
|
98
|
+
"cost_usd": total,
|
|
99
|
+
"data_url": getattr(ans, "data_url", None),
|
|
100
|
+
"tools_used": [
|
|
101
|
+
{"name": getattr(tool, "name", None), "id": getattr(tool, "id", None)}
|
|
102
|
+
for tool in (getattr(ans, "tools_used", None) or [])
|
|
103
|
+
],
|
|
104
|
+
"outcome_type": getattr(ans, "outcome_type", None),
|
|
105
|
+
"stop_reason": getattr(ans, "stop_reason", None),
|
|
106
|
+
"duration_ms": getattr(ans, "duration_ms", None),
|
|
107
|
+
}
|
|
108
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
109
|
+
|
|
110
|
+
chart_urls = _chart_urls_from_result(ans)
|
|
111
|
+
if chart_urls:
|
|
112
|
+
for index, url in enumerate(chart_urls, start=1):
|
|
113
|
+
print(f"CHART_URL_{index}: {url}")
|
|
114
|
+
else:
|
|
115
|
+
print("CHART_URL_1: none")
|
|
116
|
+
print(f"COST_USD: {total if total is not None else 'unknown'}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def run_hermes_style_query(question: str) -> None:
|
|
120
|
+
from ctxprotocol import ContextClient, __version__
|
|
121
|
+
|
|
122
|
+
print(f"ctxprotocol version: {__version__}", file=sys.stderr)
|
|
123
|
+
print(f"favorites_only: {FAVORITES_ONLY}", file=sys.stderr)
|
|
124
|
+
print(f"query: {question[:120]}{'...' if len(question) > 120 else ''}", file=sys.stderr)
|
|
125
|
+
|
|
126
|
+
async with ContextClient(
|
|
127
|
+
api_key=load_api_key(),
|
|
128
|
+
request_timeout_seconds=float(SDK_QUERY_TIMEOUT),
|
|
129
|
+
stream_timeout_seconds=float(SDK_QUERY_TIMEOUT),
|
|
130
|
+
) as client:
|
|
131
|
+
answer = await client.query.run(
|
|
132
|
+
query=question,
|
|
133
|
+
response_shape="answer_with_evidence",
|
|
134
|
+
include_data=False,
|
|
135
|
+
include_data_url=True,
|
|
136
|
+
favorites_only=FAVORITES_ONLY,
|
|
137
|
+
)
|
|
138
|
+
emit(answer)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main() -> None:
|
|
142
|
+
question = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_QUERY
|
|
143
|
+
try:
|
|
144
|
+
asyncio.run(run_hermes_style_query(question))
|
|
145
|
+
except Exception as exc: # noqa: BLE001
|
|
146
|
+
print(json.dumps({"error": f"{type(exc).__name__}: {exc}"}, indent=2))
|
|
147
|
+
print("CHART_URL_1: none")
|
|
148
|
+
print("COST_USD: unknown")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Throwaway: same Hermes kwargs but via stream() (direct /api/v1/query SSE).
|
|
3
|
+
|
|
4
|
+
Use to compare job-backed run() vs held-open sync path on long queries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
SDK_QUERY_TIMEOUT = 1860
|
|
16
|
+
FAVORITES_ONLY = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_api_key() -> str:
|
|
20
|
+
path = os.path.abspath(
|
|
21
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "context-sdk", ".env.local")
|
|
22
|
+
)
|
|
23
|
+
with open(path) as handle:
|
|
24
|
+
for line in handle:
|
|
25
|
+
if line.startswith("CONTEXT_API_KEY="):
|
|
26
|
+
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
27
|
+
raise SystemExit("CONTEXT_API_KEY missing")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def main() -> None:
|
|
31
|
+
query_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
|
|
32
|
+
os.path.dirname(__file__), "saudi-oil-query.txt"
|
|
33
|
+
)
|
|
34
|
+
with open(query_path) as handle:
|
|
35
|
+
query = handle.read().strip()
|
|
36
|
+
|
|
37
|
+
from ctxprotocol import ContextClient, __version__
|
|
38
|
+
|
|
39
|
+
print(f"stream path | ctxprotocol {__version__}", file=sys.stderr)
|
|
40
|
+
started = time.time()
|
|
41
|
+
async with ContextClient(
|
|
42
|
+
api_key=load_api_key(),
|
|
43
|
+
request_timeout_seconds=float(SDK_QUERY_TIMEOUT),
|
|
44
|
+
stream_timeout_seconds=float(SDK_QUERY_TIMEOUT),
|
|
45
|
+
) as client:
|
|
46
|
+
result = None
|
|
47
|
+
async for event in client.query.stream(
|
|
48
|
+
query=query,
|
|
49
|
+
response_shape="answer_with_evidence",
|
|
50
|
+
include_data=False,
|
|
51
|
+
include_data_url=True,
|
|
52
|
+
favorites_only=FAVORITES_ONLY,
|
|
53
|
+
):
|
|
54
|
+
if event.type == "done":
|
|
55
|
+
result = event.result
|
|
56
|
+
elif event.type == "error":
|
|
57
|
+
raise RuntimeError(getattr(event, "message", str(event)))
|
|
58
|
+
|
|
59
|
+
elapsed = time.time() - started
|
|
60
|
+
cost = getattr(getattr(result, "cost", None), "total_cost_usd", None)
|
|
61
|
+
charts = [
|
|
62
|
+
getattr(art, "url", None)
|
|
63
|
+
for art in (getattr(result, "computed_artifacts", None) or [])
|
|
64
|
+
if getattr(art, "kind", None) == "image" and getattr(art, "url", None)
|
|
65
|
+
]
|
|
66
|
+
print(
|
|
67
|
+
json.dumps(
|
|
68
|
+
{
|
|
69
|
+
"elapsed_s": round(elapsed, 1),
|
|
70
|
+
"stop_reason": getattr(result, "stop_reason", None),
|
|
71
|
+
"cost_usd": cost,
|
|
72
|
+
"chart_count": len(charts),
|
|
73
|
+
"tools_used": [
|
|
74
|
+
getattr(t, "name", None)
|
|
75
|
+
for t in (getattr(result, "tools_used", None) or [])
|
|
76
|
+
],
|
|
77
|
+
"response_preview": (getattr(result, "response", None) or "")[:800],
|
|
78
|
+
},
|
|
79
|
+
indent=2,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
for index, url in enumerate(charts, start=1):
|
|
83
|
+
print(f"CHART_URL_{index}: {url}")
|
|
84
|
+
if not charts:
|
|
85
|
+
print("CHART_URL_1: none")
|
|
86
|
+
print(f"COST_USD: {cost}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
try:
|
|
91
|
+
asyncio.run(main())
|
|
92
|
+
except Exception as exc: # noqa: BLE001
|
|
93
|
+
print(json.dumps({"error": f"{type(exc).__name__}: {exc}"}, indent=2))
|
|
94
|
+
print("CHART_URL_1: none")
|
|
95
|
+
print("COST_USD: unknown")
|
|
96
|
+
sys.exit(1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Compare the current price and 7-day trend of Bitcoin and Ethereum, and find one prediction market related to Bitcoin's price this year with its current odds. Provide a short verdict on whether crypto positioning looks bullish or bearish. Text only, no charts needed.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
A news outlet reported: Saudi Arabia slashed the August price of its flagship Arab Light crude for Asia by $11 a barrel — the biggest reduction in at least 26 years. The move reflects weaker oil markets, rising global supply, and intensifying competition among Gulf producers.
|
|
2
|
+
|
|
3
|
+
I'm crafting a single tweet for a data-driven Twitter account (@ctxprotocol). The tweet needs one compelling data point and a chart that makes someone stop scrolling.
|
|
4
|
+
|
|
5
|
+
Search your available tools for data relevant to this story. Use whatever tools fit best — prediction markets, equities intelligence, crypto data, crypto positioning, trading intelligence, or any combination. You don't need to use every tool; pick the ones that produce the most insight for THIS specific story.
|
|
6
|
+
|
|
7
|
+
For each relevant market or asset you find, I need:
|
|
8
|
+
- Current key metrics (oil price, energy stock price, odds, spread, flow — whatever the tool provides)
|
|
9
|
+
- How has the key metric moved over the market's lifetime or let the backend determine the meaningful time range
|
|
10
|
+
- Positioning/flow: who is on which side? Is smart money aligned with or against the narrative?
|
|
11
|
+
- Catalyst date: when does this market resolve, or when is the next event that could move it? Include for every market/asset found.
|
|
12
|
+
- Follow-up assessment: is this worth revisiting in a future post? What event would trigger a followup?
|
|
13
|
+
|
|
14
|
+
Deliver a verdict: Was the market surprised by this news, or was it already priced in? What is the single most surprising data point? If you synthesized across multiple tools, what does the combined picture reveal that no single tool shows alone?
|
|
15
|
+
|
|
16
|
+
Analysis and Chart: You are a master quantitative analyst. Based on all the data you've gathered, what is the most meaningful analysis? Create the most compelling possible chart(s). When the data spans multiple distinct dimensions, splitting into focused charts often produces clearer visuals. Each chart should capture one dimension of the story clearly. Charts must have clean labels, readable legends, and professional styling. Include a labeled chart summary for EACH chart produced, describing what data it shows and the key visual takeaway.
|
|
@@ -397,57 +397,89 @@ def _make_done_stream_response(result: dict[str, Any]) -> _FakeStreamResponse:
|
|
|
397
397
|
)
|
|
398
398
|
|
|
399
399
|
|
|
400
|
+
MOCK_RUN_JOB_ID = "11111111-1111-4111-8111-111111111111"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _job_start_payload() -> dict[str, Any]:
|
|
404
|
+
"""202 payload returned when the durable job behind run() is started."""
|
|
405
|
+
return {
|
|
406
|
+
"status": "running",
|
|
407
|
+
"jobId": MOCK_RUN_JOB_ID,
|
|
408
|
+
"pollingTool": "context_query_poll",
|
|
409
|
+
"message": "running",
|
|
410
|
+
"progress": None,
|
|
411
|
+
"querySession": None,
|
|
412
|
+
"createdAt": "2026-06-14T00:00:00.000Z",
|
|
413
|
+
"updatedAt": "2026-06-14T00:00:00.000Z",
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _job_completed_payload(result: dict[str, Any]) -> dict[str, Any]:
|
|
418
|
+
"""Completed job-status payload carrying the final query result."""
|
|
419
|
+
return {
|
|
420
|
+
"status": "completed",
|
|
421
|
+
"jobId": MOCK_RUN_JOB_ID,
|
|
422
|
+
"progress": None,
|
|
423
|
+
"querySession": None,
|
|
424
|
+
"result": result,
|
|
425
|
+
"error": None,
|
|
426
|
+
"createdAt": "2026-06-14T00:00:00.000Z",
|
|
427
|
+
"updatedAt": "2026-06-14T00:01:00.000Z",
|
|
428
|
+
"completedAt": "2026-06-14T00:01:00.000Z",
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _job_run_side_effect(result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
433
|
+
"""Mock the job-start + completed-status pair backing run() since 0.21.0."""
|
|
434
|
+
return [_job_start_payload(), _job_completed_payload(result)]
|
|
435
|
+
|
|
436
|
+
|
|
400
437
|
# ============================================================================
|
|
401
438
|
# Tests: query.run()
|
|
402
439
|
# ============================================================================
|
|
403
440
|
|
|
404
441
|
|
|
405
442
|
class TestQueryRun:
|
|
406
|
-
"""Tests for client.query.run() —
|
|
443
|
+
"""Tests for client.query.run() — durable job path (start + poll) since 0.21.0."""
|
|
407
444
|
|
|
408
445
|
async def test_sends_correct_request_body_string(self) -> None:
|
|
409
|
-
"""String shorthand
|
|
446
|
+
"""String shorthand starts a durable job, then polls its status."""
|
|
410
447
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
411
448
|
|
|
412
|
-
with patch.object(
|
|
413
|
-
|
|
414
|
-
) as mock_stream:
|
|
415
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
449
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
450
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
416
451
|
await client.query.run("What are the top whale movements?")
|
|
417
452
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
453
|
+
assert mock_fetch.await_count == 2
|
|
454
|
+
start_call = mock_fetch.await_args_list[0]
|
|
455
|
+
assert start_call.args[0] == "/api/v1/query/jobs"
|
|
456
|
+
assert start_call.kwargs["method"] == "POST"
|
|
457
|
+
assert start_call.kwargs["json_body"] == {
|
|
458
|
+
"query": "What are the top whale movements?",
|
|
459
|
+
"stream": False,
|
|
460
|
+
}
|
|
461
|
+
assert start_call.kwargs["extra_headers"] is None
|
|
462
|
+
status_call = mock_fetch.await_args_list[1]
|
|
463
|
+
assert status_call.args[0] == f"/api/v1/query/jobs/{MOCK_RUN_JOB_ID}"
|
|
427
464
|
|
|
428
465
|
async def test_sends_correct_request_body_with_tools(self) -> None:
|
|
429
|
-
"""Options dict with tools sends tool IDs."""
|
|
466
|
+
"""Options dict with tools sends tool IDs to the job start endpoint."""
|
|
430
467
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
431
468
|
|
|
432
|
-
with patch.object(
|
|
433
|
-
|
|
434
|
-
) as mock_stream:
|
|
435
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
469
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
470
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
436
471
|
await client.query.run(
|
|
437
472
|
query="Analyze whale activity",
|
|
438
473
|
tools=["tool-uuid-1", "tool-uuid-2"],
|
|
439
474
|
)
|
|
440
475
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
},
|
|
449
|
-
extra_headers=None,
|
|
450
|
-
)
|
|
476
|
+
start_call = mock_fetch.await_args_list[0]
|
|
477
|
+
assert start_call.args[0] == "/api/v1/query/jobs"
|
|
478
|
+
assert start_call.kwargs["json_body"] == {
|
|
479
|
+
"query": "Analyze whale activity",
|
|
480
|
+
"tools": ["tool-uuid-1", "tool-uuid-2"],
|
|
481
|
+
"stream": False,
|
|
482
|
+
}
|
|
451
483
|
|
|
452
484
|
async def test_forwards_model_and_data_options(self) -> None:
|
|
453
485
|
"""Optional model and data controls are forwarded in request body."""
|
|
@@ -461,10 +493,8 @@ class TestQueryRun:
|
|
|
461
493
|
"orchestrationMetrics": MOCK_ORCHESTRATION_METRICS,
|
|
462
494
|
}
|
|
463
495
|
|
|
464
|
-
with patch.object(
|
|
465
|
-
|
|
466
|
-
) as mock_stream:
|
|
467
|
-
mock_stream.return_value = _make_done_stream_response(success_with_data)
|
|
496
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
497
|
+
mock_fetch.side_effect = _job_run_side_effect(success_with_data)
|
|
468
498
|
result = await client.query.run(
|
|
469
499
|
query="Analyze whale activity",
|
|
470
500
|
agent_model_id="kimi-k2.6-model",
|
|
@@ -474,20 +504,17 @@ class TestQueryRun:
|
|
|
474
504
|
include_developer_trace=True,
|
|
475
505
|
)
|
|
476
506
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
},
|
|
489
|
-
extra_headers=None,
|
|
490
|
-
)
|
|
507
|
+
start_call = mock_fetch.await_args_list[0]
|
|
508
|
+
assert start_call.args[0] == "/api/v1/query/jobs"
|
|
509
|
+
assert start_call.kwargs["json_body"] == {
|
|
510
|
+
"query": "Analyze whale activity",
|
|
511
|
+
"stream": False,
|
|
512
|
+
"agentModelId": "kimi-k2.6-model",
|
|
513
|
+
"responseShape": "answer_with_evidence",
|
|
514
|
+
"includeData": True,
|
|
515
|
+
"includeDataUrl": True,
|
|
516
|
+
"includeDeveloperTrace": True,
|
|
517
|
+
}
|
|
491
518
|
assert result.data == {"summary": "tool output"}
|
|
492
519
|
assert (
|
|
493
520
|
result.data_url
|
|
@@ -503,10 +530,8 @@ class TestQueryRun:
|
|
|
503
530
|
"""developerTrace payload is deserialized into QueryResult."""
|
|
504
531
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
505
532
|
|
|
506
|
-
with patch.object(
|
|
507
|
-
|
|
508
|
-
) as mock_stream:
|
|
509
|
-
mock_stream.return_value = _make_done_stream_response(
|
|
533
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
534
|
+
mock_fetch.side_effect = _job_run_side_effect(
|
|
510
535
|
{
|
|
511
536
|
**MOCK_SUCCESS_RESPONSE,
|
|
512
537
|
"developerTrace": MOCK_DEVELOPER_TRACE,
|
|
@@ -538,10 +563,8 @@ class TestQueryRun:
|
|
|
538
563
|
"""Query result keeps developer_trace unset when API omits it."""
|
|
539
564
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
540
565
|
|
|
541
|
-
with patch.object(
|
|
542
|
-
|
|
543
|
-
) as mock_stream:
|
|
544
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
566
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
567
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
545
568
|
result = await client.query.run("test query")
|
|
546
569
|
|
|
547
570
|
assert result.developer_trace is None
|
|
@@ -550,10 +573,8 @@ class TestQueryRun:
|
|
|
550
573
|
"""When include_developer_trace is set, SDK synthesizes fallback trace if API omits it."""
|
|
551
574
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
552
575
|
|
|
553
|
-
with patch.object(
|
|
554
|
-
|
|
555
|
-
) as mock_stream:
|
|
556
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
576
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
577
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
557
578
|
result = await client.query.run(
|
|
558
579
|
query="test query",
|
|
559
580
|
include_developer_trace=True,
|
|
@@ -618,10 +639,8 @@ class TestQueryRun:
|
|
|
618
639
|
"""resume_from is forwarded to the query endpoint."""
|
|
619
640
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
620
641
|
|
|
621
|
-
with patch.object(
|
|
622
|
-
|
|
623
|
-
) as mock_stream:
|
|
624
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
642
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
643
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
625
644
|
await client.query.run(
|
|
626
645
|
query="test query",
|
|
627
646
|
resume_from={
|
|
@@ -630,7 +649,7 @@ class TestQueryRun:
|
|
|
630
649
|
},
|
|
631
650
|
)
|
|
632
651
|
|
|
633
|
-
body =
|
|
652
|
+
body = mock_fetch.await_args_list[0].kwargs["json_body"]
|
|
634
653
|
assert body["resumeFrom"] == {
|
|
635
654
|
"sessionId": "11111111-1111-4111-8111-111111111111",
|
|
636
655
|
"attemptId": "22222222-2222-4222-8222-222222222222",
|
|
@@ -658,10 +677,8 @@ class TestQueryRun:
|
|
|
658
677
|
},
|
|
659
678
|
}
|
|
660
679
|
|
|
661
|
-
with patch.object(
|
|
662
|
-
|
|
663
|
-
) as mock_stream:
|
|
664
|
-
mock_stream.return_value = _make_done_stream_response(response)
|
|
680
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
681
|
+
mock_fetch.side_effect = _job_run_side_effect(response)
|
|
665
682
|
result = await client.query.run("test query")
|
|
666
683
|
|
|
667
684
|
assert result.query_session is not None
|
|
@@ -672,35 +689,31 @@ class TestQueryRun:
|
|
|
672
689
|
"""Explicit idempotency key is forwarded as request header."""
|
|
673
690
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
674
691
|
|
|
675
|
-
with patch.object(
|
|
676
|
-
|
|
677
|
-
) as mock_stream:
|
|
678
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
692
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
693
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
679
694
|
await client.query.run(
|
|
680
695
|
query="Analyze whale activity",
|
|
681
696
|
tools=["tool-uuid-1"],
|
|
682
697
|
idempotency_key="7b31f437-61ed-4f76-8a6e-ed2f0766ffb8",
|
|
683
698
|
)
|
|
684
699
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
start_call = mock_fetch.await_args_list[0]
|
|
701
|
+
assert start_call.args[0] == "/api/v1/query/jobs"
|
|
702
|
+
assert start_call.kwargs["json_body"] == {
|
|
703
|
+
"query": "Analyze whale activity",
|
|
704
|
+
"tools": ["tool-uuid-1"],
|
|
705
|
+
"stream": False,
|
|
706
|
+
}
|
|
707
|
+
assert start_call.kwargs["extra_headers"] == {
|
|
708
|
+
"Idempotency-Key": "7b31f437-61ed-4f76-8a6e-ed2f0766ffb8"
|
|
709
|
+
}
|
|
695
710
|
|
|
696
711
|
async def test_parses_success_response_into_query_result(self) -> None:
|
|
697
712
|
"""Successful API response is deserialized into QueryResult."""
|
|
698
713
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
699
714
|
|
|
700
|
-
with patch.object(
|
|
701
|
-
|
|
702
|
-
) as mock_stream:
|
|
703
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_SUCCESS_RESPONSE)
|
|
715
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
716
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_SUCCESS_RESPONSE)
|
|
704
717
|
result = await client.query.run("test query")
|
|
705
718
|
|
|
706
719
|
assert isinstance(result, QueryResult)
|
|
@@ -723,10 +736,8 @@ class TestQueryRun:
|
|
|
723
736
|
"""Structured evidence fields are exposed on QueryResult."""
|
|
724
737
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
725
738
|
|
|
726
|
-
with patch.object(
|
|
727
|
-
|
|
728
|
-
) as mock_stream:
|
|
729
|
-
mock_stream.return_value = _make_done_stream_response(MOCK_EVIDENCE_RESPONSE)
|
|
739
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
740
|
+
mock_fetch.side_effect = _job_run_side_effect(MOCK_EVIDENCE_RESPONSE)
|
|
730
741
|
result = await client.query.run(
|
|
731
742
|
query="Where is BTC flowing today?",
|
|
732
743
|
response_shape="evidence_only",
|
|
@@ -746,10 +757,8 @@ class TestQueryRun:
|
|
|
746
757
|
"""Shared SDK fixture preserves computed artifacts and grounding."""
|
|
747
758
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
748
759
|
|
|
749
|
-
with patch.object(
|
|
750
|
-
|
|
751
|
-
) as mock_stream:
|
|
752
|
-
mock_stream.return_value = _make_done_stream_response(
|
|
760
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
761
|
+
mock_fetch.side_effect = _job_run_side_effect(
|
|
753
762
|
SHARED_QUERY_FIXTURE["groundedAnswer"]
|
|
754
763
|
)
|
|
755
764
|
result = await client.query.run(
|
|
@@ -864,10 +873,8 @@ class TestQueryRun:
|
|
|
864
873
|
],
|
|
865
874
|
}
|
|
866
875
|
|
|
867
|
-
with patch.object(
|
|
868
|
-
|
|
869
|
-
) as mock_stream:
|
|
870
|
-
mock_stream.return_value = _make_done_stream_response(response)
|
|
876
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
877
|
+
mock_fetch.side_effect = _job_run_side_effect(response)
|
|
871
878
|
result = await client.query.run("Render richer chart artifacts")
|
|
872
879
|
|
|
873
880
|
assert result.computed_artifacts is not None
|
|
@@ -913,10 +920,8 @@ class TestQueryRun:
|
|
|
913
920
|
},
|
|
914
921
|
],
|
|
915
922
|
}
|
|
916
|
-
with patch.object(
|
|
917
|
-
|
|
918
|
-
) as mock_stream:
|
|
919
|
-
mock_stream.return_value = _make_done_stream_response(response)
|
|
923
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
924
|
+
mock_fetch.side_effect = _job_run_side_effect(response)
|
|
920
925
|
result = await client.query.run("Render a chart image")
|
|
921
926
|
|
|
922
927
|
assert result.computed_artifacts is not None
|
|
@@ -934,10 +939,8 @@ class TestQueryRun:
|
|
|
934
939
|
"""Ungrounded runtime outcomes deserialize as capability_miss."""
|
|
935
940
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
936
941
|
|
|
937
|
-
with patch.object(
|
|
938
|
-
|
|
939
|
-
) as mock_stream:
|
|
940
|
-
mock_stream.return_value = _make_done_stream_response(
|
|
942
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
943
|
+
mock_fetch.side_effect = _job_run_side_effect(
|
|
941
944
|
SHARED_QUERY_FIXTURE["ungroundedCapabilityMiss"]
|
|
942
945
|
)
|
|
943
946
|
result = await client.query.run(
|
|
@@ -957,10 +960,8 @@ class TestQueryRun:
|
|
|
957
960
|
"""Structured capability miss results deserialize into QueryResult."""
|
|
958
961
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
959
962
|
|
|
960
|
-
with patch.object(
|
|
961
|
-
|
|
962
|
-
) as mock_stream:
|
|
963
|
-
mock_stream.return_value = _make_done_stream_response(
|
|
963
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
964
|
+
mock_fetch.side_effect = _job_run_side_effect(
|
|
964
965
|
MOCK_CAPABILITY_MISS_RESULT
|
|
965
966
|
)
|
|
966
967
|
result = await client.query.run(
|
|
@@ -977,43 +978,50 @@ class TestQueryRun:
|
|
|
977
978
|
]
|
|
978
979
|
assert len(result.capability_miss.suggested_rewrites) == 3
|
|
979
980
|
|
|
980
|
-
async def
|
|
981
|
-
"""Run raises when the
|
|
981
|
+
async def test_raises_if_job_completes_without_result(self) -> None:
|
|
982
|
+
"""Run raises when the job completes but carries no result payload."""
|
|
982
983
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
983
984
|
|
|
984
|
-
with patch.object(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
985
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
986
|
+
mock_fetch.side_effect = [
|
|
987
|
+
_job_start_payload(),
|
|
988
|
+
{
|
|
989
|
+
**_job_completed_payload(MOCK_SUCCESS_RESPONSE),
|
|
990
|
+
"result": None,
|
|
991
|
+
},
|
|
992
|
+
]
|
|
988
993
|
|
|
989
994
|
with pytest.raises(ContextError) as exc_info:
|
|
990
995
|
await client.query.run("test query")
|
|
991
996
|
|
|
992
|
-
assert "
|
|
997
|
+
assert "completed without a result" in str(exc_info.value)
|
|
993
998
|
|
|
994
|
-
async def
|
|
995
|
-
"""Run raises the
|
|
999
|
+
async def test_raises_context_error_when_job_fails(self) -> None:
|
|
1000
|
+
"""Run raises the failed job's error message."""
|
|
996
1001
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
997
1002
|
|
|
998
|
-
with patch.object(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1003
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
1004
|
+
mock_fetch.side_effect = [
|
|
1005
|
+
_job_start_payload(),
|
|
1006
|
+
{
|
|
1007
|
+
**_job_completed_payload(MOCK_SUCCESS_RESPONSE),
|
|
1008
|
+
"status": "failed",
|
|
1009
|
+
"result": None,
|
|
1010
|
+
"error": "Query failed before completion (query_failed)",
|
|
1011
|
+
},
|
|
1012
|
+
]
|
|
1002
1013
|
|
|
1003
1014
|
with pytest.raises(ContextError) as exc_info:
|
|
1004
1015
|
await client.query.run("test query")
|
|
1005
1016
|
|
|
1006
1017
|
assert "Query failed before completion" in str(exc_info.value)
|
|
1007
|
-
assert exc_info.value.code == "query_failed"
|
|
1008
1018
|
|
|
1009
1019
|
async def test_raises_context_error_on_no_wallet(self) -> None:
|
|
1010
1020
|
"""no_wallet error is propagated correctly."""
|
|
1011
1021
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
1012
1022
|
|
|
1013
|
-
with patch.object(
|
|
1014
|
-
|
|
1015
|
-
) as mock_stream:
|
|
1016
|
-
mock_stream.side_effect = ContextError(
|
|
1023
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
1024
|
+
mock_fetch.side_effect = ContextError(
|
|
1017
1025
|
message="Account not fully set up.",
|
|
1018
1026
|
code="no_wallet",
|
|
1019
1027
|
status_code=400,
|
|
@@ -1028,10 +1036,8 @@ class TestQueryRun:
|
|
|
1028
1036
|
"""query_failed error is propagated correctly."""
|
|
1029
1037
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
1030
1038
|
|
|
1031
|
-
with patch.object(
|
|
1032
|
-
|
|
1033
|
-
) as mock_stream:
|
|
1034
|
-
mock_stream.side_effect = ContextError(
|
|
1039
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
1040
|
+
mock_fetch.side_effect = ContextError(
|
|
1035
1041
|
message="Query failed: Tool execution timed out",
|
|
1036
1042
|
code="query_failed",
|
|
1037
1043
|
status_code=422,
|
|
@@ -1046,10 +1052,8 @@ class TestQueryRun:
|
|
|
1046
1052
|
"""HTTP errors before stream setup still propagate correctly."""
|
|
1047
1053
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
1048
1054
|
|
|
1049
|
-
with patch.object(
|
|
1050
|
-
|
|
1051
|
-
) as mock_stream:
|
|
1052
|
-
mock_stream.side_effect = ContextError(
|
|
1055
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
1056
|
+
mock_fetch.side_effect = ContextError(
|
|
1053
1057
|
message="Insufficient funds. Set a spending cap in the dashboard.",
|
|
1054
1058
|
code="insufficient_allowance",
|
|
1055
1059
|
status_code=402,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/client/polymarket_query_trace_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/server/hummingbot-contributor/env.example
RENAMED
|
File without changes
|
{ctxprotocol-0.20.0 → ctxprotocol-0.22.0}/examples/server/hummingbot-contributor/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|