ctxprotocol 0.19.1__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.1 → ctxprotocol-0.20.0}/PKG-INFO +21 -7
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/README.md +20 -6
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/query.py +76 -6
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/pyproject.toml +1 -1
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_query.py +41 -1
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/uv.lock +1 -1
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/.codexignore +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/.gitignore +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/CHANGELOG.md +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/auth/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/client.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/developer.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/discovery.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/tools.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/types.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/hyperliquid.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/polymarket.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/wallet.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/core.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/trace.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/types.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/validation.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/handshake/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/py.typed +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/docs/README.md +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/execute_client.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/polymarket_query_trace_validation.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/query_client.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/test_favorites_only.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/test_get_tool.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/two_surfaces_client.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/README.md +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/env.example +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/requirements.txt +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/server.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/polymarket-query-trace-results-py.json +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/__init__.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/contrib_search_validation_cases.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_client.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_contrib_search.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_discovery.py +0 -0
- {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_tools.py +0 -0
|
@@ -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."""
|
|
@@ -376,10 +389,18 @@ class Query:
|
|
|
376
389
|
async def poll(
|
|
377
390
|
self,
|
|
378
391
|
job_id: str,
|
|
379
|
-
interval_ms: int =
|
|
380
|
-
timeout_ms: int =
|
|
392
|
+
interval_ms: int = DEFAULT_QUERY_POLL_INTERVAL_MS,
|
|
393
|
+
timeout_ms: int = DEFAULT_QUERY_POLL_TIMEOUT_MS,
|
|
381
394
|
) -> QueryJobStatusResult:
|
|
382
|
-
"""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
|
+
"""
|
|
383
404
|
loop = asyncio.get_running_loop()
|
|
384
405
|
deadline = loop.time() + timeout_ms / 1000
|
|
385
406
|
|
|
@@ -389,10 +410,59 @@ class Query:
|
|
|
389
410
|
return status
|
|
390
411
|
if status.status == "failed":
|
|
391
412
|
raise ContextError(status.error or "Context query job failed")
|
|
392
|
-
|
|
413
|
+
remaining_s = deadline - loop.time()
|
|
414
|
+
if remaining_s <= 0:
|
|
415
|
+
break
|
|
416
|
+
await asyncio.sleep(min(interval_ms / 1000, remaining_s))
|
|
393
417
|
|
|
394
418
|
raise ContextError(f"Context query job polling timed out after {timeout_ms}ms")
|
|
395
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
|
+
|
|
396
466
|
async def stream(
|
|
397
467
|
self,
|
|
398
468
|
query: str,
|
|
@@ -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
|
|
@@ -1631,3 +1631,43 @@ class TestQueryJobs:
|
|
|
1631
1631
|
|
|
1632
1632
|
assert status.status == "completed"
|
|
1633
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
|
|
File without changes
|
|
File without changes
|
{ctxprotocol-0.19.1 → 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.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/env.example
RENAMED
|
File without changes
|
{ctxprotocol-0.19.1 → 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
|