Metadata-Version: 2.2
Name: gpt-client-lite
Version: 1.0.0
Summary: Minimal OpenAI GPT client for Python - chat, embeddings, functions, streaming, no heavy deps
Author-email: Vladyslav Zaiets <rutova2@gmail.com>
License: MIT
Project-URL: Homepage, https://sarmkadan.com
Project-URL: Repository, https://github.com/Sarmkadan/gpt-client-lite
Keywords: openai,gpt,chatgpt,api,client,llm,ai,lightweight
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE

# gpt-client-lite

Minimal OpenAI GPT client for Python — chat, embeddings, function calling,
and streaming with **zero heavy dependencies**.

Built entirely on the Python standard library (`urllib`, `json`,
`dataclasses`). Works on Python 3.9 and above.

## Install

```bash
pip install gpt-client-lite
```

## Quick start

```python
from gpt_client_lite import GPTClient

client = GPTClient()              # reads OPENAI_API_KEY from environment
# client = GPTClient("sk-...")   # or pass the key directly
```

---

## Chat

### Single-turn prompt

```python
reply = client.chat.simple("What is the capital of Japan?")
print(reply)  # "Tokyo"
```

### With a system prompt

```python
reply = client.chat.simple(
    "Tell me a joke.",
    system="You are a stand-up comedian who only tells puns.",
)
```

### Full response object

```python
from gpt_client_lite import Message

resp = client.chat.complete(
    [Message.user("Explain black holes in one sentence.")],
    model="gpt-4o",
    temperature=0.7,
    max_tokens=120,
)

print(resp.text)          # assistant reply
print(resp.usage)         # Usage(prompt=..., completion=..., total=...)
print(resp.finish_reason) # "stop"
```

### JSON mode

```python
data = client.chat.json("List 5 world capitals as a JSON array of strings.")
# data is already a parsed Python object: ["Tokyo", "Paris", ...]
```

### Summarise / translate helpers

```python
short = client.chat.summarise(long_article, max_words=80)
es    = client.chat.translate("Good morning, have a great day!", "Spanish")
```

### Multi-turn conversation

```python
conv = client.chat.conversation(system="You are a helpful travel guide.")

print(conv.say("I'm visiting Tokyo next month."))
print(conv.say("What neighbourhood should I stay in?"))
print(conv.say("What's the food like there?"))

# Inspect history
for msg in conv.history:
    print(f"[{msg.role}] {msg.content[:60]}")

conv.clear()  # reset (keeps system message by default)
```

---

## Streaming

```python
# Print tokens to stdout as they arrive
client.streaming.simple("Write a haiku about the ocean").print_stream()

# Iterate manually
for chunk in client.streaming.simple("Count from 1 to 5."):
    print(chunk.text, end="", flush=True)

# Callback style
full_text = client.streaming.simple("Tell me a story").on_token(
    lambda token: print(token, end="", flush=True)
)

# Collect the whole text at once (buffers internally)
text = client.streaming.simple("Explain recursion.").text
```

### Streaming from a messages list

```python
from gpt_client_lite import Message

msgs = [
    Message.system("You are a Python expert."),
    Message.user("Show me a quick sort implementation."),
]

stream = client.streaming.chat(msgs, temperature=0.2)
stream.print_stream()
```

---

## Embeddings

```python
# Embed a single text
vec = client.embeddings.embed("The quick brown fox")
print(len(vec))  # 1536 for text-embedding-3-small

# Embed multiple texts (one API call)
vecs = client.embeddings.embed_batch(["cat", "dog", "car", "truck"])

# Semantic similarity (0 – 1)
score = client.embeddings.similarity("puppy", "dog")
print(f"{score:.3f}")  # ~0.92

# Find most similar
results = client.embeddings.most_similar(
    "machine learning",
    ["deep learning", "pasta cooking", "neural networks", "astronomy"],
    top_k=2,
)
for text, score in results:
    print(f"{score:.3f}  {text}")

# Rank documents by relevance
ranked = client.embeddings.rank("Python async programming", documents)
for idx, text, score in ranked:
    print(f"[{idx}] {score:.3f}  {text[:60]}")

# Cluster texts (pure-Python k-means, no numpy)
groups = client.embeddings.cluster(sentences, n_clusters=3)
```

---

## Function / Tool Calling

```python
import json
from gpt_client_lite import Message

# Register functions — schema is inferred from type hints + docstring
@client.functions.register
def get_weather(location: str, unit: str = "celsius") -> str:
    """Get the current weather for a location.

    Args:
        location: City name or address.
        unit: Temperature unit — celsius or fahrenheit.
    """
    # Your real implementation here
    return json.dumps({"location": location, "temperature": 22, "unit": unit})


@client.functions.register
def search_web(query: str, max_results: int = 5) -> str:
    """Search the web and return a summary.

    Args:
        query: Search query string.
        max_results: Maximum number of results to return.
    """
    return f"Top {max_results} results for: {query}"


# Single round
messages = [Message.user("What's the weather like in Paris?")]
tools    = client.functions.build_tools()
resp     = client.chat.with_tools(messages, tools=tools)

if resp.has_tool_calls:
    # Execute and get ready-to-send result messages
    tool_results = client.functions.execute_all_tool_calls(resp.tool_calls)

    messages.append(resp.message.to_dict())
    messages.extend(tool_results)

    final = client.chat.complete(messages)
    print(final.text)
```

### Agentic loop (auto-executes until the model stops calling tools)

```python
messages = [Message.user("Search for Python news and summarise the weather in London.")]

final = client.functions.run_agentic_loop(
    messages=[m.to_dict() for m in messages],
    chat_api=client.chat,
    max_rounds=5,
)
print(final.text)
```

---

## Configuration

```python
client = GPTClient(
    api_key="sk-...",          # or OPENAI_API_KEY env var
    model="gpt-4o",            # default model for chat + streaming
    timeout=30,                # seconds (default 60)
    max_retries=5,             # exponential backoff retries (default 3)
    organization="org-...",    # optional OpenAI org ID
    project="proj-...",        # optional project ID
    base_url="https://...",    # override for proxies / compatible APIs
    extra_headers={"X-Custom": "value"},
)

# Change the default model after construction
client.set_model("gpt-4o-mini")
```

---

## Error handling

```python
from gpt_client_lite import (
    AuthenticationError,
    RateLimitError,
    ContextLengthExceededError,
    InvalidRequestError,
    APIError,
    GPTClientError,
)

try:
    reply = client.chat.simple("Hello!")
except AuthenticationError:
    print("Invalid API key.")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}s")
except ContextLengthExceededError:
    print("Prompt too long for this model.")
except APIError as e:
    print(f"Server error {e.status_code}: {e}")
except GPTClientError as e:
    print(f"Unexpected error: {e}")
```

---

## API Reference

### `GPTClient`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `api_key` | `str` | `$OPENAI_API_KEY` | API key |
| `model` | `str` | `"gpt-4o-mini"` | Default model |
| `timeout` | `int` | `60` | Request timeout in seconds |
| `max_retries` | `int` | `3` | Retry attempts for transient errors |
| `organization` | `str` | `None` | OpenAI organization ID |
| `project` | `str` | `None` | OpenAI project ID |
| `base_url` | `str` | OpenAI API | Override base URL |
| `extra_headers` | `dict` | `None` | Additional HTTP headers |

### `client.chat`

| Method | Returns | Description |
|---|---|---|
| `.simple(prompt, system, **kw)` | `str` | One-shot prompt |
| `.complete(messages, **kw)` | `ChatResponse` | Full completions call |
| `.json(prompt, system, **kw)` | `Any` | JSON-mode, auto-parsed |
| `.summarise(text, max_words)` | `str` | Summarise text |
| `.translate(text, language)` | `str` | Translate text |
| `.with_tools(messages, tools)` | `ChatResponse` | Tool-calling request |
| `.conversation(system, **kw)` | `Conversation` | Multi-turn session |

### `client.streaming`

| Method | Returns | Description |
|---|---|---|
| `.simple(prompt, system, **kw)` | `StreamResult` | Stream a prompt |
| `.chat(messages, **kw)` | `StreamResult` | Stream a messages list |
| `.complete_to_string(msgs, on_token)` | `str` | Stream + collect |

### `StreamResult`

| Method / Property | Description |
|---|---|
| `iter_text()` | Iterator over text fragments |
| `collect()` / `.text` | Buffer and return full text |
| `print_stream()` | Write to stdout, return full text |
| `on_token(callback)` | Call `callback(token)` per fragment |
| `to_message()` | Return buffered result as `Message` |

### `client.embeddings`

| Method | Returns | Description |
|---|---|---|
| `.embed(text)` | `List[float]` | Single embedding vector |
| `.embed_batch(texts)` | `List[List[float]]` | Batch embedding |
| `.similarity(a, b)` | `float` | Semantic similarity 0–1 |
| `.most_similar(query, candidates, top_k)` | `List[(str, float)]` | Nearest texts |
| `.rank(query, documents)` | `List[(int, str, float)]` | Ranked with original index |
| `.cluster(texts, n_clusters)` | `List[List[str]]` | K-means clustering |

### `client.functions`

| Method | Description |
|---|---|
| `@.register` | Decorator to register a function |
| `.build_tools(names?)` | List of `FunctionDefinition` for the API |
| `.execute_tool_call(tc)` | Execute one tool call dict |
| `.execute_all_tool_calls(tcs)` | Execute all, return result messages |
| `.run_agentic_loop(messages, chat_api)` | Auto loop until no more tool calls |

---

## License

MIT © [Vladyslav Zaiets](https://sarmkadan.com)
