Metadata-Version: 2.4
Name: chat-completions-conversation-with-tools
Version: 0.1.0a3
Summary: Chat Completions conversation helper with tool support for Python 2 and Python 3.
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/jifengwu2k/chat-completions-conversation-with-tools
Project-URL: Bug Tracker, https://github.com/jifengwu2k/chat-completions-conversation-with-tools/issues
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=2
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: create-inspect-typeddict
Requires-Dist: get-args-and-origin
Requires-Dist: typing; python_version < "3.5"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# Chat Completions Conversation With Tools

**LLM tool calling for environments that modern SDKs left behind.**

Python 2. Python 3. Windows XP. Embedded CPython. Air-gapped networks. If you can run `import json`, you can run this package.

---

## Why This Exists

LLM tool calling has become essential — but the ecosystem has raced toward modern stacks. The official OpenAI Python SDK requires Python 3.8+. LangChain wants 3.9+. They pull in `httpx`, `pydantic`, `anyio`, and a tree of native extensions. None of that runs on a Windows XP box or an old Debian embedded controller.

Meanwhile, the API itself hasn't changed: it's still JSON over HTTP, with the same OpenAI Chat Completions function-calling schema that every major provider speaks. The barrier is purely in the client libraries.

This package removes that barrier. It's a single-file, zero-dependency stateful wrapper around any OpenAI Chat Completions-compatible HTTP API. No `requests`. No `httpx`. No native code. Just `urllib`/`urllib2`, `json`, and the typing module — all of which ship with Python 2.7 and later.

## What It Gives You

| Capability | How |
|---|---|
| Tool definitions | Python `TypedDict` → JSON Schema (automatic, recursive, cycle-safe) |
| Non-streaming responses | Parse `tool_calls[]` from the response message |
| Streaming responses | SSE event parser (CRLF, comments, `[DONE]`) with tool call delta accumulation |
| Conversation history | Automatic message bookkeeping (user, assistant, tool roles) |
| Multimodal | Image URLs via OpenAI content-parts format |
| Providers | OpenAI, DeepSeek, Groq, Together, xAI, OpenRouter — anything that speaks Chat Completions |

## Function Calling Convention

This package implements the **OpenAI native function calling convention** — an API-level protocol where tools and tool calls are first-class fields in the request/response schema. This is the industry standard, supported by OpenAI and every major compatible provider.

### Request format

```json
{
  "model": "...",
  "messages": [...],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Look up the weather for a city.",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string" }
          },
          "required": ["city"],
          "additionalProperties": false
        }
      }
    }
  ],
  "tool_choice": "auto"
}
```

### Response format

```json
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "get_weather",
            "arguments": "{\"city\":\"Tokyo\"}"
          }
        }
      ]
    }
  }]
}
```

### Tool results

```json
{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "Sunny, 22°C"
}
```

### Streaming deltas

When `stream: true`, tool calls arrive incrementally via SSE chunks keyed by `index`. The package accumulates fragments and finalizes them when the stream completes.

The stream is parsed as SSE events, not raw lines: `\r\n` and `\n` endings, comment lines, `data:` fields with or without a space, multi-line `data` blocks, and the `data: [DONE]` sentinel are all handled. Servers that omit the blank line between JSON events are tolerated, and chunks without a `choices` entry (for example usage payloads) are skipped.

This is an **API-native** convention — tools and tool calls are structural fields in the HTTP bodies, not prompt-engineered text that requires client-side regex or XML parsing.

## Installation

```bash
pip install chat-completions-conversation-with-tools
```

No native extensions. No system dependencies. Works with pip on Python 2.7 and Python 3.x.

## Usage

Actual tool execution is application-defined. Provide a callable per tool, or bridge to an external process — the package only handles the API conversation.

### Basic example (OpenAI)

```python
from typing import TypedDict
from chat_completions_conversation_with_tools import (
    ChatCompletionsConversationWithTools,
    Tool,
)

class WeatherArgs(TypedDict):
    city: str

conversation = ChatCompletionsConversationWithTools(
    api_key="sk-...",
    base_url="https://api.openai.com/v1",
    model="gpt-4o",
    system_prompt="Call the get_weather tool when asked about weather.",
    tools_by_name={
        "get_weather": Tool("Look up the current weather for a city.", WeatherArgs),
    },
)

# Non-streaming
response = conversation.send_and_receive_response("What's the weather in Tokyo?")
print(response.tool_calls)
# [ToolCall(id='...', name='get_weather', arguments={'city': 'Tokyo'})]

conversation.append_tool_message(response.tool_calls[0].id, "Sunny, 22°C")
final = conversation.send_and_receive_response()
print(final.content)
# "The weather in Tokyo is sunny with a temperature of 22°C."
```

### Streaming (DeepSeek)

```python
conversation = ChatCompletionsConversationWithTools(
    api_key="sk-...",
    base_url="https://api.deepseek.com",
    model="deepseek-chat",
    system_prompt="Call the get_weather tool when asked about weather.",
    tools_by_name={
        "get_weather": Tool("Look up the current weather for a city.", WeatherArgs),
    },
)

response = conversation.send_and_stream_response(
    text="What's the weather in Tokyo?",
    on_content_delta=lambda text: print(text, end="", flush=True),
)
# Let me check the weather in Tokyo for you!
print(response.tool_calls)
# [ToolCall(id='call_00_...', name='get_weather', arguments={'city': 'Tokyo'})]

conversation.append_tool_message(response.tool_calls[0].id, "Sunny, 22°C")
final = conversation.send_and_stream_response(
    on_content_delta=lambda text: print(text, end="", flush=True),
)
# The weather in Tokyo is sunny with a temperature of 22°C.
```

### Image input

```python
conversation.append_user_message(
    "What's in this image?",
    image_url="https://example.com/photo.jpg",
)
response = conversation.send_and_receive_response()
```

## Public API

| Class | Purpose |
|---|---|
| `Tool(description, typeddict_class)` | Wraps a tool description and a TypedDict parameter schema. Automatically converts the TypedDict to JSON Schema. |
| `ToolCall` | Stores a parsed tool call: `.id`, `.name`, `.arguments`. |
| `AssistantResponse` | Stores the assistant turn: `.content` (text) and `.tool_calls` (list of `ToolCall`). |
| `ChatCompletionsConversationWithTools` | Manages message history and API communication. |

### `ChatCompletionsConversationWithTools`

```
__init__(api_key, base_url, model, system_prompt, tools_by_name)
```

| Method | Description |
|---|---|
| `send_and_receive_response(text=None, image_url=None)` | Send a user message (or continue after tool results) and return the assistant response. |
| `send_and_stream_response(text=None, image_url=None, on_content_delta=None, on_tool_call_delta=None)` | Same, but streams the response via callbacks. |
| `append_user_message(text, image_url=None)` | Add a user message to history without sending. |
| `append_assistant_message(content, tool_calls=None)` | Add an assistant message to history. |
| `append_tool_message(tool_call_id, content)` | Add a tool result to history. |
| `to_messages()` | Return a deep, JSON-safe copy of the conversation history. |
| `reset()` | Clear conversation history and re-insert the system prompt. |
| `set_system_prompt(prompt, update_messages=True)` | Change the system prompt. |
| `build_payload(messages, stream=False)` | Build the raw API request payload (for debugging or custom transport). |

### Saving and loading conversations

The conversation history is a plain JSON-safe list of messages, so it can be
saved and restored without any custom wire format. Serialization and
deserialization between this list and files or strings is left to the standard
library `json` module.

```python
import json

from chat_completions_conversation_with_tools import (
    ChatCompletionsConversationWithTools,
    append_messages,
    load_messages,
)

conversation = ChatCompletionsConversationWithTools(
    api_key="...", base_url="...", model="...",
    system_prompt="...", tools_by_name={},
)
conversation.append_user_message("Hello")

# Save: get a deep, JSON-safe copy of the history...
messages = conversation.to_messages()

# ...and hand it to the json module for file or string I/O.
with open("conversation.json", "w", encoding="utf-8") as handle:
    json.dump(messages, handle, indent=2)

# Load: deserialize with json, validate with load_messages...
with open("conversation.json", "r", encoding="utf-8") as handle:
    saved = load_messages(json.load(handle))

# ...and append it to a new (or existing) conversation to continue it.
continued = ChatCompletionsConversationWithTools(
    api_key="...", base_url="...", model="...",
    system_prompt="...", tools_by_name={},
)
append_messages(continued, saved)
```

The file format is a bare JSON array of API message objects, so a saved
transcript can also be used directly as an API request body.

| Function | Description |
|---|---|
| `conversation.to_messages()` | Deep, JSON-safe copy of the history (mutating it does not affect the conversation). |
| `load_messages(messages)` | Validate a message list (roles, `tool_call_id` on tool messages) and return an independent copy. |
| `append_messages(conversation, messages)` | Append validated messages to a conversation; skips `system` messages, since the system prompt is configured at construction. Returns the number of messages appended. |

## Constraints (By Design)

**No `requests` or `httpx`.** This package uses only `urllib`/`urllib2` from the standard library. It installs and runs anywhere Python does — no native compilation, no system libraries.

**Python 2.7 compatible.** Every string, every import, every type annotation is written to work on Python 2.7 and Python 3.x from the same source file.

**Single file.** The entire library is one module. Drop `chat_completions_conversation_with_tools.py` into any project. No package structure to navigate, no implicit namespace dependencies.

**No framework dependencies.** No `pydantic`, no `attrs`, no `dataclasses`. Tool parameter schemas use standard `TypedDict`, automatically converted to JSON Schema at call time.

## Provider Compatibility

Tested and working with:

| Provider | Model | Status |
|---|---|---|
| DeepSeek | `deepseek-chat` | ✅ Non-streaming + streaming |
| OpenAI | `gpt-4o`, `gpt-4.1` | ✅ Compatible API |
| Groq, Together, xAI, OpenRouter | Any Chat Completions model | ✅ Compatible API |
| Local (vLLM, Ollama, LM Studio) | Any tool-capable model | ✅ Compatible API |

## License

MIT — see [LICENSE](LICENSE).
