ctxprotocol 0.19.0__tar.gz → 0.20.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.19.0 → ctxprotocol-0.20.0}/CHANGELOG.md +4 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/PKG-INFO +21 -7
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/README.md +20 -6
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/__init__.py +1 -1
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/query.py +80 -8
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/pyproject.toml +1 -1
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/test_query.py +74 -5
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/uv.lock +1 -1
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/.codexignore +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/.gitignore +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/auth/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/client.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/developer.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/discovery.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/tools.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/client/types.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/context/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/context/hyperliquid.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/context/polymarket.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/context/wallet.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/contrib/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/core.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/trace.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/types.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/validation.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/handshake/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/ctxprotocol/py.typed +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/docs/README.md +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/execute_client.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/polymarket_query_trace_validation.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/query_client.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/test_favorites_only.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/test_get_tool.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/two_surfaces_client.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/README.md +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/env.example +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/requirements.txt +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/server.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/polymarket-query-trace-results-py.json +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/__init__.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/contrib_search_validation_cases.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/test_client.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/test_contrib_search.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/test_discovery.py +0 -0
- {ctxprotocol-0.19.0 → ctxprotocol-0.20.0}/tests/test_tools.py +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.1
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
3
7
|
## 0.19.0
|
|
4
8
|
|
|
5
9
|
- Fixed `client.query.run()`/`client.query.stream()` raising a `ValidationError` on queries that returned a rendered image artifact (e.g. server-rendered chart PNGs). `computedArtifacts` now accepts both `chart` (structured spec + data) and `image` (hosted URL) variants.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxprotocol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.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,21 +149,35 @@ print(
|
|
|
149
149
|
print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
For long-running questions,
|
|
152
|
+
For long-running questions from LLM agents, use `run_or_poll()` so the entire wait happens inside one SDK call (one model turn):
|
|
153
153
|
|
|
154
154
|
```python
|
|
155
|
-
|
|
155
|
+
answer = await client.query.run_or_poll(
|
|
156
156
|
query="Build a chart-ready dataset of Base whale movements over the last 30 days",
|
|
157
157
|
response_shape="evidence_only",
|
|
158
158
|
include_data_url=True,
|
|
159
159
|
)
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
print(answer.data_url)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The defaults are already agent-friendly: the SDK checks status every 5 seconds
|
|
165
|
+
over plain HTTP (this costs no model tokens — token cost comes from model
|
|
166
|
+
*turns*, not HTTP polls) and waits up to 31 minutes, slightly beyond the
|
|
167
|
+
hosted 1800-second compute ceiling that bounds every query path. Treat a
|
|
168
|
+
failed job-window status as terminal and start a fresh query.
|
|
169
|
+
|
|
170
|
+
For programmatic workflows that want the job handle, start a durable job and poll until it completes:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
job = await client.query.start(
|
|
174
|
+
query="Build a chart-ready dataset of Base whale movements over the last 30 days",
|
|
175
|
+
response_shape="evidence_only",
|
|
176
|
+
include_data_url=True,
|
|
165
177
|
)
|
|
166
178
|
|
|
179
|
+
completed = await client.query.poll(job.job_id)
|
|
180
|
+
|
|
167
181
|
print(completed.result.data_url if completed.result else None)
|
|
168
182
|
```
|
|
169
183
|
|
|
@@ -111,21 +111,35 @@ print(
|
|
|
111
111
|
print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
For long-running questions,
|
|
114
|
+
For long-running questions from LLM agents, use `run_or_poll()` so the entire wait happens inside one SDK call (one model turn):
|
|
115
115
|
|
|
116
116
|
```python
|
|
117
|
-
|
|
117
|
+
answer = await client.query.run_or_poll(
|
|
118
118
|
query="Build a chart-ready dataset of Base whale movements over the last 30 days",
|
|
119
119
|
response_shape="evidence_only",
|
|
120
120
|
include_data_url=True,
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
print(answer.data_url)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The defaults are already agent-friendly: the SDK checks status every 5 seconds
|
|
127
|
+
over plain HTTP (this costs no model tokens — token cost comes from model
|
|
128
|
+
*turns*, not HTTP polls) and waits up to 31 minutes, slightly beyond the
|
|
129
|
+
hosted 1800-second compute ceiling that bounds every query path. Treat a
|
|
130
|
+
failed job-window status as terminal and start a fresh query.
|
|
131
|
+
|
|
132
|
+
For programmatic workflows that want the job handle, start a durable job and poll until it completes:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
job = await client.query.start(
|
|
136
|
+
query="Build a chart-ready dataset of Base whale movements over the last 30 days",
|
|
137
|
+
response_shape="evidence_only",
|
|
138
|
+
include_data_url=True,
|
|
127
139
|
)
|
|
128
140
|
|
|
141
|
+
completed = await client.query.poll(job.job_id)
|
|
142
|
+
|
|
129
143
|
print(completed.result.data_url if completed.result else None)
|
|
130
144
|
```
|
|
131
145
|
|
|
@@ -11,11 +11,12 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import json
|
|
14
|
-
from
|
|
14
|
+
from collections.abc import AsyncGenerator
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
15
16
|
|
|
16
17
|
from ctxprotocol.client.types import (
|
|
17
|
-
ContextError,
|
|
18
18
|
AgentModelId,
|
|
19
|
+
ContextError,
|
|
19
20
|
QueryAttemptReference,
|
|
20
21
|
QueryDeveloperTrace,
|
|
21
22
|
QueryForkReference,
|
|
@@ -34,6 +35,18 @@ from ctxprotocol.client.types import (
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from ctxprotocol.client.client import ContextClient
|
|
36
37
|
|
|
38
|
+
# Internal HTTP status-check cadence for poll()/run_or_poll(). This is plain
|
|
39
|
+
# HTTP polling below any LLM boundary — it costs no model tokens no matter how
|
|
40
|
+
# frequent it is; a slower interval only delays completion detection. To keep
|
|
41
|
+
# LLM-agent token costs down, minimize *model turns* (use run_or_poll()), not
|
|
42
|
+
# HTTP polls.
|
|
43
|
+
DEFAULT_QUERY_POLL_INTERVAL_MS = 5_000
|
|
44
|
+
# Default client-side wait: slightly beyond the hosted 1800s compute ceiling
|
|
45
|
+
# (Vercel extended max duration) that bounds every query path, so the client
|
|
46
|
+
# observes the job's terminal state instead of giving up while the server is
|
|
47
|
+
# still legitimately working.
|
|
48
|
+
DEFAULT_QUERY_POLL_TIMEOUT_MS = 31 * 60_000
|
|
49
|
+
|
|
37
50
|
|
|
38
51
|
class Query:
|
|
39
52
|
"""Query resource for pay-per-response agentic queries."""
|
|
@@ -62,8 +75,9 @@ class Query:
|
|
|
62
75
|
) -> dict[str, Any]:
|
|
63
76
|
request_body: dict[str, Any] = {
|
|
64
77
|
"query": query,
|
|
65
|
-
"tools": tools,
|
|
66
78
|
}
|
|
79
|
+
if tools is not None:
|
|
80
|
+
request_body["tools"] = tools
|
|
67
81
|
if stream is not None:
|
|
68
82
|
request_body["stream"] = stream
|
|
69
83
|
if resume_from is not None:
|
|
@@ -375,10 +389,18 @@ class Query:
|
|
|
375
389
|
async def poll(
|
|
376
390
|
self,
|
|
377
391
|
job_id: str,
|
|
378
|
-
interval_ms: int =
|
|
379
|
-
timeout_ms: int =
|
|
392
|
+
interval_ms: int = DEFAULT_QUERY_POLL_INTERVAL_MS,
|
|
393
|
+
timeout_ms: int = DEFAULT_QUERY_POLL_TIMEOUT_MS,
|
|
380
394
|
) -> QueryJobStatusResult:
|
|
381
|
-
"""Poll a durable async query job until completion or failure.
|
|
395
|
+
"""Poll a durable async query job until completion or failure.
|
|
396
|
+
|
|
397
|
+
``timeout_ms`` controls how long this client waits; the hosted job
|
|
398
|
+
itself is bounded by the 1800s server compute ceiling. If the status
|
|
399
|
+
endpoint reports the job exceeded the server-side window, the job is
|
|
400
|
+
terminal and should not be polled again. ``interval_ms`` is an HTTP
|
|
401
|
+
check cadence with no effect on LLM token usage — leave it at the
|
|
402
|
+
fast default.
|
|
403
|
+
"""
|
|
382
404
|
loop = asyncio.get_running_loop()
|
|
383
405
|
deadline = loop.time() + timeout_ms / 1000
|
|
384
406
|
|
|
@@ -388,10 +410,59 @@ class Query:
|
|
|
388
410
|
return status
|
|
389
411
|
if status.status == "failed":
|
|
390
412
|
raise ContextError(status.error or "Context query job failed")
|
|
391
|
-
|
|
413
|
+
remaining_s = deadline - loop.time()
|
|
414
|
+
if remaining_s <= 0:
|
|
415
|
+
break
|
|
416
|
+
await asyncio.sleep(min(interval_ms / 1000, remaining_s))
|
|
392
417
|
|
|
393
418
|
raise ContextError(f"Context query job polling timed out after {timeout_ms}ms")
|
|
394
419
|
|
|
420
|
+
async def run_or_poll(
|
|
421
|
+
self,
|
|
422
|
+
query: str,
|
|
423
|
+
tools: list[str] | None = None,
|
|
424
|
+
favorites_only: bool | None = None,
|
|
425
|
+
agent_model_id: AgentModelId | str | None = None,
|
|
426
|
+
response_shape: QueryResponseShape | None = None,
|
|
427
|
+
include_data: bool | None = None,
|
|
428
|
+
include_data_url: bool | None = None,
|
|
429
|
+
include_developer_trace: bool | None = None,
|
|
430
|
+
resume_from: QueryAttemptReference | dict[str, Any] | None = None,
|
|
431
|
+
fork_from: QueryForkReference | dict[str, Any] | None = None,
|
|
432
|
+
idempotency_key: str | None = None,
|
|
433
|
+
interval_ms: int = DEFAULT_QUERY_POLL_INTERVAL_MS,
|
|
434
|
+
timeout_ms: int = DEFAULT_QUERY_POLL_TIMEOUT_MS,
|
|
435
|
+
) -> QueryResult:
|
|
436
|
+
"""Run a durable query job and wait internally for completion.
|
|
437
|
+
|
|
438
|
+
This is the recommended one-call helper for LLM agents: the entire
|
|
439
|
+
wait happens inside this single call (one model turn), instead of one
|
|
440
|
+
turn per ``get_status()`` check. It also avoids starting a duplicate
|
|
441
|
+
paid query after a client-side streaming timeout. The job is bounded
|
|
442
|
+
by the 1800s hosted compute ceiling.
|
|
443
|
+
"""
|
|
444
|
+
job = await self.start(
|
|
445
|
+
query=query,
|
|
446
|
+
tools=tools,
|
|
447
|
+
favorites_only=favorites_only,
|
|
448
|
+
agent_model_id=agent_model_id,
|
|
449
|
+
response_shape=response_shape,
|
|
450
|
+
include_data=include_data,
|
|
451
|
+
include_data_url=include_data_url,
|
|
452
|
+
include_developer_trace=include_developer_trace,
|
|
453
|
+
resume_from=resume_from,
|
|
454
|
+
fork_from=fork_from,
|
|
455
|
+
idempotency_key=idempotency_key,
|
|
456
|
+
)
|
|
457
|
+
completed = await self.poll(
|
|
458
|
+
job.job_id,
|
|
459
|
+
interval_ms=interval_ms,
|
|
460
|
+
timeout_ms=timeout_ms,
|
|
461
|
+
)
|
|
462
|
+
if completed.status == "completed" and completed.result is not None:
|
|
463
|
+
return completed.result
|
|
464
|
+
raise ContextError(completed.error or "Context query job completed without a result")
|
|
465
|
+
|
|
395
466
|
async def stream(
|
|
396
467
|
self,
|
|
397
468
|
query: str,
|
|
@@ -443,9 +514,10 @@ class Query:
|
|
|
443
514
|
"""
|
|
444
515
|
request_body: dict[str, Any] = {
|
|
445
516
|
"query": query,
|
|
446
|
-
"tools": tools,
|
|
447
517
|
"stream": True,
|
|
448
518
|
}
|
|
519
|
+
if tools is not None:
|
|
520
|
+
request_body["tools"] = tools
|
|
449
521
|
if resume_from is not None:
|
|
450
522
|
request_body["resumeFrom"] = (
|
|
451
523
|
resume_from.model_dump(by_alias=True, exclude_none=True)
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any
|
|
14
|
-
from unittest.mock import AsyncMock,
|
|
14
|
+
from unittest.mock import AsyncMock, patch
|
|
15
15
|
|
|
16
16
|
import httpx
|
|
17
17
|
import pytest
|
|
@@ -420,7 +420,6 @@ class TestQueryRun:
|
|
|
420
420
|
method="POST",
|
|
421
421
|
json_body={
|
|
422
422
|
"query": "What are the top whale movements?",
|
|
423
|
-
"tools": None,
|
|
424
423
|
"stream": True,
|
|
425
424
|
},
|
|
426
425
|
extra_headers=None,
|
|
@@ -480,7 +479,6 @@ class TestQueryRun:
|
|
|
480
479
|
method="POST",
|
|
481
480
|
json_body={
|
|
482
481
|
"query": "Analyze whale activity",
|
|
483
|
-
"tools": None,
|
|
484
482
|
"stream": True,
|
|
485
483
|
"agentModelId": "kimi-k2.6-model",
|
|
486
484
|
"responseShape": "answer_with_evidence",
|
|
@@ -1091,7 +1089,6 @@ class TestQueryStream:
|
|
|
1091
1089
|
method="POST",
|
|
1092
1090
|
json_body={
|
|
1093
1091
|
"query": "What are whale movements?",
|
|
1094
|
-
"tools": None,
|
|
1095
1092
|
"stream": True,
|
|
1096
1093
|
},
|
|
1097
1094
|
extra_headers=None,
|
|
@@ -1534,7 +1531,6 @@ class TestQueryJobs:
|
|
|
1534
1531
|
method="POST",
|
|
1535
1532
|
json_body={
|
|
1536
1533
|
"query": "long query",
|
|
1537
|
-
"tools": None,
|
|
1538
1534
|
"stream": False,
|
|
1539
1535
|
"responseShape": "evidence_only",
|
|
1540
1536
|
"includeDataUrl": True,
|
|
@@ -1544,6 +1540,39 @@ class TestQueryJobs:
|
|
|
1544
1540
|
},
|
|
1545
1541
|
)
|
|
1546
1542
|
|
|
1543
|
+
async def test_start_omits_null_optional_fields(self) -> None:
|
|
1544
|
+
"""Regression: optional fields must be absent (not JSON null) in the job body.
|
|
1545
|
+
|
|
1546
|
+
The /api/v1/query/jobs endpoint validates the body with a Zod schema whose
|
|
1547
|
+
optional fields accept `undefined`/absent but reject `null`. Sending
|
|
1548
|
+
`"tools": null` (as the SDK previously did) made query.start() 400 with
|
|
1549
|
+
"Invalid query job request". Optional fields must be omitted when unset,
|
|
1550
|
+
matching the TS SDK (JSON.stringify drops undefined keys).
|
|
1551
|
+
"""
|
|
1552
|
+
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
1553
|
+
payload = {
|
|
1554
|
+
"status": "running",
|
|
1555
|
+
"jobId": "11111111-1111-4111-8111-111111111111",
|
|
1556
|
+
"pollingTool": "context_query_status",
|
|
1557
|
+
"message": "running",
|
|
1558
|
+
"progress": None,
|
|
1559
|
+
"querySession": None,
|
|
1560
|
+
"createdAt": "2026-06-14T00:00:00.000Z",
|
|
1561
|
+
"updatedAt": "2026-06-14T00:00:00.000Z",
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
1565
|
+
mock_fetch.return_value = payload
|
|
1566
|
+
await client.query.start(query="long query")
|
|
1567
|
+
|
|
1568
|
+
body = mock_fetch.call_args.kwargs["json_body"]
|
|
1569
|
+
# No optional field may serialize to JSON null.
|
|
1570
|
+
assert all(value is not None for value in body.values()), body
|
|
1571
|
+
# tools specifically must be omitted when not supplied (was the 400 trigger).
|
|
1572
|
+
assert "tools" not in body, body
|
|
1573
|
+
# query is always present and non-null.
|
|
1574
|
+
assert body["query"] == "long query"
|
|
1575
|
+
|
|
1547
1576
|
async def test_get_status_fetches_query_job(self) -> None:
|
|
1548
1577
|
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
1549
1578
|
payload = {
|
|
@@ -1602,3 +1631,43 @@ class TestQueryJobs:
|
|
|
1602
1631
|
|
|
1603
1632
|
assert status.status == "completed"
|
|
1604
1633
|
assert mock_fetch.await_count == 2
|
|
1634
|
+
|
|
1635
|
+
async def test_run_or_poll_returns_completed_query_result(self) -> None:
|
|
1636
|
+
client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
|
|
1637
|
+
start_payload = {
|
|
1638
|
+
"status": "running",
|
|
1639
|
+
"jobId": "11111111-1111-4111-8111-111111111111",
|
|
1640
|
+
"pollingTool": "context_query_poll",
|
|
1641
|
+
"message": "running",
|
|
1642
|
+
"progress": None,
|
|
1643
|
+
"querySession": None,
|
|
1644
|
+
"createdAt": "2026-06-14T00:00:00.000Z",
|
|
1645
|
+
"updatedAt": "2026-06-14T00:00:00.000Z",
|
|
1646
|
+
}
|
|
1647
|
+
completed_payload = {
|
|
1648
|
+
"status": "completed",
|
|
1649
|
+
"jobId": "11111111-1111-4111-8111-111111111111",
|
|
1650
|
+
"progress": None,
|
|
1651
|
+
"querySession": None,
|
|
1652
|
+
"result": MOCK_SUCCESS_RESPONSE,
|
|
1653
|
+
"error": None,
|
|
1654
|
+
"createdAt": "2026-06-14T00:00:00.000Z",
|
|
1655
|
+
"updatedAt": "2026-06-14T00:01:00.000Z",
|
|
1656
|
+
"completedAt": "2026-06-14T00:01:00.000Z",
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
|
|
1660
|
+
mock_fetch.side_effect = [start_payload, completed_payload]
|
|
1661
|
+
result = await client.query.run_or_poll(
|
|
1662
|
+
"Analyze whale activity",
|
|
1663
|
+
interval_ms=1,
|
|
1664
|
+
timeout_ms=1000,
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
assert result.response == MOCK_SUCCESS_RESPONSE["response"]
|
|
1668
|
+
assert mock_fetch.await_count == 2
|
|
1669
|
+
assert mock_fetch.await_args_list[0].args[0] == "/api/v1/query/jobs"
|
|
1670
|
+
assert (
|
|
1671
|
+
mock_fetch.await_args_list[1].args[0]
|
|
1672
|
+
== "/api/v1/query/jobs/11111111-1111-4111-8111-111111111111"
|
|
1673
|
+
)
|
|
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
|
|
File without changes
|
{ctxprotocol-0.19.0 → ctxprotocol-0.20.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.19.0 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/env.example
RENAMED
|
File without changes
|
{ctxprotocol-0.19.0 → ctxprotocol-0.20.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
|