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
28 changes: 26 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,34 @@ jobs:
pip install ".[dev]"

- name: Run Ruff linter
run: ruff check .
run: ruff check . ../examples/langchain

- name: Run Ruff formatter check
run: ruff format --check .
run: ruff format --check . ../examples/langchain

langchain-example:
name: LangChain Example (${{ matrix.adapter-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
adapter-version: ["1.6.6", "1.7.0"]
defaults:
run:
working-directory: examples/langchain
steps:
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install example dependencies
run: python -m pip install 'langchain-openai==${{ matrix.adapter-version }}' pytest

- name: Test ChatOpenAI protocol
run: python -m pytest -q

python-test:
name: Python Test (${{ matrix.python-version }})
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ Use this application key for API requests. SDK account-management methods requir
| TypeScript | [typescript](typescript/) | `@acedatacloud/sdk` |
| Go | [go](go/) | `github.com/AceDataCloud/SDK/go` |

## Framework examples

- [LangChain chat](examples/langchain/): use `langchain-openai` with the
OpenAI-compatible Chat Completions endpoint for text, streaming, and tools.

## Architecture

All three SDKs share the same design, organised along **two axes**:
Expand Down
95 changes: 95 additions & 0 deletions examples/langchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# LangChain chat with Ace Data Cloud

Ace Data Cloud's `/v1/chat/completions` endpoint works with LangChain's existing
[`ChatOpenAI`](https://docs.langchain.com/oss/python/integrations/chat/openai)
adapter. No separate package is needed for standard text, streaming, and tool
calling. `ChatOpenAI` only preserves standard OpenAI response fields; use a
provider-specific API when you need nonstandard fields or operations.

## Run

Use Python 3.10 or newer. Download the example and create a separate environment:

```bash
git clone https://github.com/AceDataCloud/SDK.git
cd SDK
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r examples/langchain/requirements.txt pytest
```

On Windows, activate the environment with `.venv\Scripts\Activate.ps1` in
PowerShell. The example pins `langchain-openai==1.7.0`; CI also checks 1.6.6.

Get an application API key from
[Ace Data Cloud → Applications](https://platform.acedata.cloud/console/applications?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=sdk_langchain_readme_api_key).
Sign in or register, initialize **General application** if needed, and open
**Keys / Manage Keys → Create** to give this integration a separate key.
Check its service access, expiration, allowed APIs, usage limit, and balance.
Use the raw API key, without `Bearer `; a `platform-...` management token cannot
replace it. See the [illustrated key setup](../../README.md#get-your-ace-data-cloud-api-key).

From the SDK repository root, set the key and run one mode:

```bash
export ACEDATACLOUD_API_TOKEN='your Ace Data Cloud token'
python3 examples/langchain/chat.py 'Reply with one short sentence.'
```

Optional streaming and tool examples each submit additional requests:

```bash
python3 examples/langchain/chat.py --mode stream 'Count from one to three.'
python3 examples/langchain/chat.py --mode tools
```

In PowerShell, set the key with `$env:ACEDATACLOUD_API_TOKEN='your token'`.
The default model is `gpt-4o-mini`; set `ACEDATACLOUD_MODEL` to another model
available to your account in the
[model directory and current pricing](https://platform.acedata.cloud/models?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=sdk_langchain_readme_pricing).
`ACEDATACLOUD_BASE_URL` defaults to
`https://api.acedata.cloud/v1` and can point to a local mock or staging endpoint.
Do not include `/chat/completions` in the base URL.

The example fixes `use_responses_api=False`, since some model names cause
LangChain to choose the Responses API. It sets `max_retries=0`, so an error is
not automatically submitted again. The `chat` and `stream` modes each make one
billable request; `tools` makes two (tool selection and the final answer). The
local `add_numbers` function has no external side effects. Any automatic retry
or agent loop you add can create more billable requests.

The example prints the API response ID and `usage_metadata` token counts when
available. Keep the response ID for troubleshooting. Token counts are not the
charged Credits; check the
[Usage History](https://platform.acedata.cloud/console/usages?utm_source=opensource&utm_medium=referral&utm_campaign=evergreen&utm_content=sdk_langchain_readme_usage) for the actual
charge. Streaming usage is off by default for compatibility. Use
`--mode stream --stream-usage` with a model that supports OpenAI's
`stream_options.include_usage` to request final token counts; if the service
does not return them, check Usage History.

The tool example should print `5` and report usage for its two API calls.
Text appears on stdout; response IDs and available token counts appear on stderr.

| Result | What to check |
| --- | --- |
| `401` | Token value and permissions. Pass the bare token; the adapter adds `Bearer`. |
| `403` | Balance, access, or content policy shown in the response. |
| `429` | Rate limit. Wait before an intentional retry. |
| `404` | Base URL must include `/v1`; confirm the model ID. |
| `5xx` or timeout | Check service status and Usage History before retrying an uncertain request. |

## Verify without a billable call

With `pytest` installed, run:

```bash
cd examples/langchain
python3 -m pytest -q
```

The tests use the installed LangChain and OpenAI SDK with a local HTTP mock.
They check URL, bearer authorization, Chat Completions request mode, response
and token usage parsing, streaming, tool round trip, and errors without making
a network request. They do not establish live account access, provider behavior,
or billing. Complete those checks with a valid token before claiming live
availability or seeking an upstream LangChain listing.
116 changes: 116 additions & 0 deletions examples/langchain/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Call Ace Data Cloud Chat Completions with LangChain's ChatOpenAI."""

import argparse
import json
import os
import sys

from langchain_core.messages import HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI

BASE_URL = "https://api.acedata.cloud/v1"
DEFAULT_MODEL = "gpt-4o-mini"
ADD_TOOL = {
"type": "function",
"function": {
"name": "add_numbers",
"description": "Add two integers.",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"},
},
"required": ["a", "b"],
"additionalProperties": False,
},
},
}


def build_model(*, stream_usage: bool = False, **client_options) -> ChatOpenAI:
token = os.environ.get("ACEDATACLOUD_API_TOKEN")
if not token:
raise SystemExit("Set ACEDATACLOUD_API_TOKEN before running this example.")
return ChatOpenAI(
model=os.environ.get("ACEDATACLOUD_MODEL", DEFAULT_MODEL),
api_key=token,
base_url=os.environ.get("ACEDATACLOUD_BASE_URL", BASE_URL),
use_responses_api=False,
stream_usage=stream_usage,
max_retries=0,
timeout=60,
**client_options,
)


def show_usage(message) -> None:
response_id = message.response_metadata.get("id")
if response_id:
print(f"response id: {response_id}", file=sys.stderr)
usage = message.usage_metadata
if usage:
print(f"tokens: {json.dumps(usage, sort_keys=True)}", file=sys.stderr)
else:
print(
"token usage unavailable; check Usage History for the charge",
file=sys.stderr,
)


def run_chat(model: ChatOpenAI, prompt: str) -> None:
reply = model.invoke([HumanMessage(content=prompt)])
print(reply.content)
show_usage(reply)


def run_stream(model: ChatOpenAI, prompt: str) -> None:
complete = None
for chunk in model.stream([HumanMessage(content=prompt)]):
if isinstance(chunk.content, str):
print(chunk.content, end="", flush=True)
complete = chunk if complete is None else complete + chunk
print()
if complete is not None:
show_usage(complete)


def run_tools(model: ChatOpenAI) -> None:
messages = [HumanMessage(content="Use add_numbers to add 2 and 3, then report the sum.")]
tool_reply = model.bind_tools([ADD_TOOL], tool_choice="required").invoke(messages)
if not tool_reply.tool_calls:
raise RuntimeError("The model returned no tool call.")
show_usage(tool_reply)
messages.append(tool_reply)
for call in tool_reply.tool_calls:
if call["name"] != "add_numbers":
raise RuntimeError(f"Unexpected tool: {call['name']}")
args = call["args"]
result = int(args["a"]) + int(args["b"])
messages.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
final = model.invoke(messages)
print(final.content)
show_usage(final)


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("prompt", nargs="?", default="Reply with one short sentence.")
parser.add_argument("--mode", choices=("chat", "stream", "tools"), default="chat")
parser.add_argument(
"--stream-usage",
action="store_true",
help="Request a final token usage chunk when streaming (model support varies).",
)
args = parser.parse_args()
model = build_model(stream_usage=args.stream_usage)
if args.mode == "chat":
run_chat(model, args.prompt)
elif args.mode == "stream":
run_stream(model, args.prompt)
else:
run_tools(model)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions examples/langchain/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
langchain-openai==1.7.0
Loading
Loading