Metadata-Version: 2.4
Name: climateclaw-client
Version: 0.2.0
Summary: ClimateClaw Client library for connecting and interacting with an instance of the ClimateClaw chatbot.
Author-email: Felix Oertel <oertel@dkrz.de>
Maintainer-email: Felix Oertel <oertel@dkrz.de>
License-Expression: EUPL-1.2
Project-URL: Homepage, https://github.com/freva-org/climateclaw-client
Project-URL: Documentation, https://climate-claw-client.readthedocs.io/en/latest/
Project-URL: Source, https://github.com/freva-org/climateclaw-client
Project-URL: Tracker, https://github.com/freva-org/climateclaw-client/issues
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx
Requires-Dist: pydantic
Requires-Dist: py_oidc_auth_client
Requires-Dist: nest_asyncio
Requires-Dist: pillow
Provides-Extra: testsite
Requires-Dist: tox; extra == "testsite"
Requires-Dist: isort; extra == "testsite"
Requires-Dist: black; extra == "testsite"
Requires-Dist: blackdoc; extra == "testsite"
Requires-Dist: flake8; extra == "testsite"
Requires-Dist: pre-commit; extra == "testsite"
Requires-Dist: mypy; extra == "testsite"
Requires-Dist: pytest; extra == "testsite"
Requires-Dist: pytest-mock; extra == "testsite"
Requires-Dist: pytest-asyncio; extra == "testsite"
Requires-Dist: pytest-cov; extra == "testsite"
Requires-Dist: pytest-httpx; extra == "testsite"
Requires-Dist: setuptools; extra == "testsite"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: pydata-sphinx-theme>=0.7; extra == "docs"
Requires-Dist: sphinx-autodoc2>=0.5.0; extra == "docs"
Requires-Dist: autodocsumm>=0.2.12; extra == "docs"
Requires-Dist: myst-parser>=2.0; extra == "docs"
Provides-Extra: dev
Requires-Dist: climateclaw_client[testsite]; extra == "dev"
Requires-Dist: climateclaw_client[docs]; extra == "dev"
Dynamic: license-file

# ClimateClaw Client

[![License](https://raw.githubusercontent.com/eClip-/EUPL-badge/refs/heads/master/eupl_1.2.svg)](LICENSE)
[![docs](https://readthedocs.org/projects/climate-claw-client/badge/?version=latest)](https://climate-claw-client.readthedocs.io/latest/?badge=latest)
[![codecov](https://codecov.io/github/freva-org/climateclaw-client/graph/badge.svg?token=kDsGq9llcK)](https://codecov.io/github/freva-org/climateclaw-client)

A Python client library for interacting with the [ClimateClaw backend](https://github.com/freva-org/climateclaw). This library provides both synchronous and asynchronous interfaces for communicating with a ClimateClaw chatbot instance.

**Features:**
- Synchronous client (`ClimateClaw`) with full API support
- Asynchronous client (`AsyncClimateClaw`) with full API support
- OIDC authentication via [py-oidc-auth-client](https://pypi.org/project/py-oidc-auth-client/)
- Thread management (create, retrieve, list, search, fork, and delete conversation threads)
- Streaming and non-streaming prompt responses
- Thread operations (stop active conversations, set thread topics, edit/fork threads)
- User feedback (submit positive/negative feedback on assistant messages)
- Rich message types with markdown rendering support

## Requirements

- Python 3.10+

## Dependencies

- `httpx` - HTTP client for making requests
- `pydantic` - Data validation and message modeling
- `py_oidc_auth_client` - OIDC authentication handling

## Installation

Currently, this package is in development and must be installed from source:

```bash
git clone https://github.com/freva-org/climateclaw-client.git
cd climateclaw-client
pip install -e .
```

Or using `uv`:

```bash
uv pip install -e .
```

## Usage

### Initialization

```python
from climateclaw_client.client import ClimateClaw

# Create a client instance
cc = ClimateClaw(
    base_url="https://your-climate-claw-backend.com",
    token_store_path="~/.cache/climateclaw-client/token-store.json",  # Optional: path to store auth tokens
)

# Authenticate with the backend (triggers OIDC flow)
cc.authenticate()
```

### Available Models

```python
# List available chatbot models
models = cc.available_models
print(f"Available models: {models}")

# Set a default model for the client
cc.model = "gpt-4.1"
```

### Prompting the Backend

#### Non-streamed Response

```python
# Send a prompt and get the complete conversation
conversation = cc.prompt(
    "Please calculate the average temperature over Germany for the years 1990-2020!"
)
# Render the entire answer as a human-readable string
print(conversation)

# Access the individual messages
for message in conversation.messages:
    print(f"{message.variant}: {message.content}")

# Get markdown representation
markdown = conversation.repr_markdown()
print(markdown)
```

#### Streamed Response

```python
# Send a prompt with streaming enabled
stream_conv = cc.prompt(
    "Please explain the ENSO phenomenon to me and give examples of how to quantify it!", stream=True
)

# Iterate over markdown-ready chunks as they arrive
with stream_conv as stream:
    for markdown_chunk in stream.iter_for_markdown():
        print(markdown_chunk)

# After streaming completes, access the full conversation
full_conversation = stream_conv.translate_to_conversation()
print(full_conversation.repr_markdown())
```

### Thread Management

#### Create a New Thread

```python
# Create a new conversation thread
thread_id = cc.newthread()
print(f"New thread ID: {thread_id}")
```

#### Get a Thread

```python
# Retrieve an existing thread by ID
thread_id = "your-thread-id"
conversation = cc.getthread(thread_id=thread_id)

# Or use the current active thread
conversation = cc.getthread()

# Print all messages
print(conversation)
```

#### Prompt in a Specific Thread

```python
# Continue a conversation in an existing thread
response = cc.prompt(
    "Please explain how the SOI can be calculated and run an example analysis.",
    thread_id=thread_id,  # optional: uses thread of active conversation otherwise
)
```

#### List User Threads

```python
# List all your conversation threads
total_threads, user_threads = cc.getuserthreads(num_threads=10)
print(f"A total number of {total_threads} threads was retrieved.")
# access individual threads (which are Conversation objects)
print(user_threads[0])
```

#### Search Threads

```python
# Search for threads by topic
total_results, matching_threads = cc.searchthreads(query="climate analysis", num_threads=5)
```

#### Set Thread Topic

```python
# Set a topic for a thread (useful for searching later)
cc.setthreadtopic("ENSO analysis", thread_id=thread_id)
```

#### Delete a Thread

```python
# Delete a thread on the backend when you're done with it
cc.deletethread(thread_id=thread_id)
```

### Working with Message Types

The client provides rich message types that can be rendered in different formats:

```python
from climateclaw_client.client import ClimateClaw

# Create a client instance
cc = ClimateClaw(base_url="https://your-climate-claw-backend.com")

# Authenticate with the backend
cc.authenticate()

# List available models
print(f"Available models: {cc.available_models}")
cc.model = cc.available_models[0]

# Start a conversation
response = cc.prompt(
    "Show me a code example of using the xarray library for analysing climate data!"
)

# Access individual messages
initial_response = response[0]

# String representation (for Python sessions)
print(str(initial_response))

# Markdown representation (for rendering)
print(initial_response.repr_markdown())

# Access content directly
print(initial_response.content)

# Extract code cells from Assistant messages
for code_cell in initial_response.message.code_cells:
    print(f"Code cell: {code_cell}")
```

### Handling Images

Image messages have special methods:

```python
# If the response contains an image
if response.messages[1].variant == "Image":
    image_message = conversation.messages[1]

    # Get markdown representation (base64 embedded)
    md = image_message.repr_markdown()

    # Save to file
    image_message.save_to_file("output.png")
```

### Raw Message Access

```python
# Access raw message chunks (before aggregation)
response = cc.prompt("Hello ClimateClaw! What is your function?")

for raw_msg in conversation.raw_messages:
    print(raw_msg.message.variant)
    print(raw_msg.message.content)
```


## Message Types

The library supports the following message variants:

| Variant | Description | Special Methods |
|---------|-------------|-----------------|
| `Prompt` | Initial user prompt | `repr_content()`, `repr_markdown()` |
| `User` | User message | `repr_content()`, `repr_markdown()` |
| `Assistant` | Assistant response | `code_cells` property, `repr_content()`, `repr_markdown()` |
| `Code` | Python code | `code_cells` property, `repr_markdown()` renders as code block |
| `CodeOutput` | Code execution output | `repr_markdown()` renders as blockquote |
| `Image` | Base64-encoded image | `repr_markdown()`, `save_to_file()` |
| `ServerError` | Server error message | `repr_content()`, `repr_markdown()` |
| `OpenAIError` | OpenAI error message | `repr_content()`, `repr_markdown()` |
| `CodeError` | Code execution error | `repr_content()`, `repr_markdown()` |
| `StreamEnd` | Stream completion marker | `repr_markdown()` |
| `ServerHint` | Backend hint data (such as server heartbeats) | `repr_markdown()` |

## Asynchronous Client

The library includes an `AsyncClimateClaw` class for async operations, providing the same functionality as the synchronous client but with async/await syntax:

```python
import asyncio
from climateclaw_client import AsyncClimateClaw


async def main():
    # Create an async client instance
    cc = AsyncClimateClaw(
        base_url="https://your-climate-claw-backend.com",
        token_store_path="~/.cache/climateclaw-client/token-store.json",
    )

    # Authenticate with the backend
    await cc.authenticate()

    # List available models
    print(f"Available models: {cc.available_models}")
    cc.model = cc.available_models[0]

    # Send a prompt
    response = await cc.prompt(
        "Please calculate the average temperature over Germany for 1990-2020!"
    )
    print(response)

    # Send a streaming prompt
    stream_resp = await cc.prompt("Please explain the ENSO phenomenon to me!", stream=True)
    async with stream_resp as stream:
        async for markdown_chunk in stream.aiter_for_markdown():
            print(markdown_chunk)

    # Thread management
    thread_id = await cc.newthread()
    response = await cc.prompt(
        "Please explain how the SOI can be calculated.",
        thread_id=thread_id,
    )


# Run the async main function
asyncio.run(main())
```

**Note:** The async client uses `httpx.AsyncClient` under the hood and provides all the same methods as the synchronous `ClimateClaw` client, but as coroutines that must be awaited.

## Configuration Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `base_url` | str/URL | Required | Base URL of the ClimateClaw backend |
| `token_store_path` | str | "" | Path to store OIDC tokens |
| `follow_redirects` | bool | True | Whether to follow HTTP redirects |
| `timeout` | float | 30.0 | Request timeout in seconds |
| `max_retries` | int | 3 | Maximum retry attempts for failed requests |
| `http_client` | httpx.Client / httpx.AsyncClient | None | Pre-configured HTTP client (Optional) |
| `thread_id` | str | None | Default thread ID for conversations |
| `model` | str | None | Default model for prompts |

**Note:** For `AsyncClimateClaw`, the `http_client` parameter should be an `httpx.AsyncClient` instance, while for `ClimateClaw` it should be an `httpx.Client` instance.


## Project Links

- **Source Code**: https://github.com/freva-org/climateclaw-client
- **Backend Repository**: https://github.com/freva-org/climateclaw
- **Documentation**: https://climate-claw-client.readthedocs.io/latest/
- **Issue Tracker**: https://github.com/freva-org/climateclaw-client/issues

## License

This project is licensed under the European Union Public Licence 1.2 (EUPL-1.2).
