Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
525 changes: 305 additions & 220 deletions README.md

Large diffs are not rendered by default.

Empty file modified bin/check-release-environment
100644 → 100755
Empty file.
50 changes: 50 additions & 0 deletions bin/check-test-server
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

function prism_is_running() {
curl --silent "http://localhost:4010" >/dev/null 2>&1
}

function is_overriding_api_base_url() {
[ -n "$API_BASE_URL" ]
}

if is_overriding_api_base_url ; then
# If someone is running the tests against the live API, we can trust they know
# what they're doing and exit early.
echo -e "${GREEN}✔ Running tests against ${API_BASE_URL}${NC}"

exit 0
elif prism_is_running ; then
echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
echo

exit 0
else
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
echo -e "running against your OpenAPI spec."
echo
echo -e "${YELLOW}To fix:${NC}"
echo
echo -e "1. Install Prism (requires Node 16+):"
echo
echo -e " With npm:"
echo -e " \$ ${YELLOW}npm install -g @stoplight/prism-cli${NC}"
echo
echo -e " With yarn:"
echo -e " \$ ${YELLOW}yarn global add @stoplight/prism-cli${NC}"
echo
echo -e "2. Run the mock server"
echo
echo -e " To run the server, pass in the path of your OpenAPI"
echo -e " spec to the prism command:"
echo
echo -e " \$ ${YELLOW}prism mock path/to/your.openapi.yml${NC}"
echo

exit 1
fi
3 changes: 3 additions & 0 deletions bin/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

bin/check-test-server && poetry run pytest "$@"
4 changes: 0 additions & 4 deletions examples/.keep

This file was deleted.

19 changes: 19 additions & 0 deletions examples/demo_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env poetry run python

import asyncio

from ctxos import AsyncCtxos


async def main() -> None:
client = AsyncCtxos()

res = await client.complete.create(
model="ctxos-1",
prompt="how does a court case get to the Supreme Court?",
max_tokens=1000,
)
print(res.choices[0].text) # type: ignore[index]


asyncio.run(main())
17 changes: 17 additions & 0 deletions examples/demo_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env poetry run python

from ctxos import Ctxos


def main() -> None:
client = Ctxos()

res = client.complete.create(
model="ctxos-1",
prompt="how does a court case get to the Supreme Court?",
max_tokens=1000,
)
print(res.choices[0].text) # type: ignore[index]


main()
46 changes: 46 additions & 0 deletions examples/streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env poetry run python

import asyncio

from ctxos import Ctxos, AsyncCtxos, APIStatusError

client = Ctxos()
async_client = AsyncCtxos()

question = """
Hey Ctxos! How can I recursively list all files in a directory in Python?
"""


def sync_request() -> None:
response = client.complete.create(
prompt=question,
model="ctxos-1",
max_tokens=300,
)
print(response.choices[0].text) # type: ignore[index]


async def async_request() -> None:
response = await async_client.complete.create(
prompt=question,
model="ctxos-1",
max_tokens=300,
)
print(response.choices[0].text) # type: ignore[index]


def request_error() -> None:
try:
client.complete.create(
prompt=question,
model="Ctxos-unknown-model",
max_tokens=300,
)
except APIStatusError as err:
print(f"Caught API status error with response body: {err.response.text}")


sync_request()
asyncio.run(async_request())
request_error()
30 changes: 30 additions & 0 deletions examples/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env poetry run python

import asyncio

from ctxos import Ctxos, AsyncCtxos


def sync_tokens() -> None:
client = Ctxos()

text = "hello world!"

tokens = client.tokens.count(input=text)
print(f"'{text}' is {tokens.tokens} tokens")


async def async_tokens() -> None:
ctxos = AsyncCtxos()

text = "first message"
tokens = await ctxos.tokens.count(input=text)
print(f"'{text}' is {tokens.tokens} tokens")

text = "second message"
tokens = await ctxos.tokens.count(input=text)
print(f"'{text}' is {tokens.tokens} tokens")


sync_tokens()
asyncio.run(async_tokens())
3 changes: 3 additions & 0 deletions src/ctxos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typing as _t

from . import types
from .tools import BaseTool as BaseTool, ToolUser as ToolUser
from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given
from ._utils import file_from_path
from ._client import Ctxos, Client, Stream, Timeout, Transport, AsyncCtxos, AsyncClient, AsyncStream, RequestOptions
Expand Down Expand Up @@ -31,6 +32,8 @@

__all__ = [
"types",
"BaseTool",
"ToolUser",
"__version__",
"__title__",
"NoneType",
Expand Down
3 changes: 3 additions & 0 deletions src/ctxos/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@

INITIAL_RETRY_DELAY = 0.5
MAX_RETRY_DELAY = 8.0

HUMAN_PROMPT = "\n\nHuman:"
AI_PROMPT = "\n\nAssistant:"
123 changes: 123 additions & 0 deletions src/ctxos/_decoders/jsonl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from __future__ import annotations

import json
from typing_extensions import Generic, TypeVar, Iterator, AsyncIterator

import httpx

from .._models import construct_type_unchecked

_T = TypeVar("_T")


class JSONLDecoder(Generic[_T]):
"""A decoder for [JSON Lines](https://jsonlines.org) format.

This class provides an iterator over a byte-iterator that parses each JSON Line
into a given type.
"""

http_response: httpx.Response
"""The HTTP response this decoder was constructed from"""

def __init__(
self,
*,
raw_iterator: Iterator[bytes],
line_type: type[_T],
http_response: httpx.Response,
) -> None:
super().__init__()
self.http_response = http_response
self._raw_iterator = raw_iterator
self._line_type = line_type
self._iterator = self.__decode__()

def close(self) -> None:
"""Close the response body stream.

This is called automatically if you consume the entire stream.
"""
self.http_response.close()

def __decode__(self) -> Iterator[_T]:
buf = b""
for chunk in self._raw_iterator:
for line in chunk.splitlines(keepends=True):
buf += line
if buf.endswith((b"\r", b"\n", b"\r\n")):
yield construct_type_unchecked(
value=json.loads(buf),
type_=self._line_type,
)
buf = b""

# flush
if buf:
yield construct_type_unchecked(
value=json.loads(buf),
type_=self._line_type,
)

def __next__(self) -> _T:
return self._iterator.__next__()

def __iter__(self) -> Iterator[_T]:
for item in self._iterator:
yield item


class AsyncJSONLDecoder(Generic[_T]):
"""A decoder for [JSON Lines](https://jsonlines.org) format.

This class provides an async iterator over a byte-iterator that parses each JSON Line
into a given type.
"""

http_response: httpx.Response

def __init__(
self,
*,
raw_iterator: AsyncIterator[bytes],
line_type: type[_T],
http_response: httpx.Response,
) -> None:
super().__init__()
self.http_response = http_response
self._raw_iterator = raw_iterator
self._line_type = line_type
self._iterator = self.__decode__()

async def close(self) -> None:
"""Close the response body stream.

This is called automatically if you consume the entire stream.
"""
await self.http_response.aclose()

async def __decode__(self) -> AsyncIterator[_T]:
buf = b""
async for chunk in self._raw_iterator:
for line in chunk.splitlines(keepends=True):
buf += line
if buf.endswith((b"\r", b"\n", b"\r\n")):
yield construct_type_unchecked(
value=json.loads(buf),
type_=self._line_type,
)
buf = b""

# flush
if buf:
yield construct_type_unchecked(
value=json.loads(buf),
type_=self._line_type,
)

async def __anext__(self) -> _T:
return await self._iterator.__anext__()

async def __aiter__(self) -> AsyncIterator[_T]:
async for item in self._iterator:
yield item
43 changes: 43 additions & 0 deletions src/ctxos/_tokenizers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from typing import cast
from pathlib import Path

from anyio import Path as AsyncPath

# tokenizers is untyped, https://github.com/huggingface/tokenizers/issues/811
# note: this comment affects the entire file
# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false, reportUnknownArgumentType=false, reportReturnType=false
from tokenizers import Tokenizer # type: ignore[import, reportMissingTypeStubs]


def _get_tokenizer_cache_path() -> Path:
return Path(__file__).parent / "tokenizer.json"


_tokenizer: Tokenizer | None = None


def _load_tokenizer(raw: str) -> "Tokenizer": # type: ignore[return-value]
global _tokenizer

_tokenizer = cast(Tokenizer, Tokenizer.from_str(raw))
return _tokenizer


def sync_get_tokenizer() -> "Tokenizer": # type: ignore[return-value]
if _tokenizer is not None:
return _tokenizer

tokenizer_path = _get_tokenizer_cache_path()
text = tokenizer_path.read_text(encoding="utf-8")
return _load_tokenizer(text)


async def async_get_tokenizer() -> "Tokenizer": # type: ignore[return-value]
if _tokenizer is not None:
return _tokenizer

tokenizer_path = AsyncPath(_get_tokenizer_cache_path())
text = await tokenizer_path.read_text(encoding="utf-8")
return _load_tokenizer(text)
7 changes: 7 additions & 0 deletions src/ctxos/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# File generated from our OpenAPI spec by Stainless.

from typing import TypeVar

from ._models import BaseModel

_BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
Loading
Loading