aissociate 0.2.0
The official Python library for the AIssociate API.
Requires Python: >=3.8

# AI:ssociate Python API Library
![Dynamic TOML Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fgitlab.com%2Faissociate%2Faissociate-python%2F-%2Fraw%2Fmain%2Fpyproject.toml%3Fref_type%3Dheads&query=%24.project.version&logo=pypi&logoColor=ffffff&label=aissociate&link=https%3A%2F%2Fpypi.org%2Fproject%2Faissociate%2F)

```aissociate``` is a Python package that provides an interface for interacting with the [AI:ssociate](https://aissociate.at) API. 
It currently only supports asynchronous clients, making it suitable for a variety of use cases. 

## Installation

You can install `aissociate` using `pip` by creating a virtual environment and installing it with 

```sh
pip install aissociate
```


## Prerequisites
- Python >=3.8 installed
- A valid API key (request one by contacting [sales@aissociate.at](mailto:sales@aissociate.at))
- `asyncio` library installed (install using `pip install asyncio`)

## Asynchronous Client

AIssociate provides an `AsyncAIssociateClient` for interacting with the streaming API asynchronously.

```python
from aissociate import AsyncAIssociateClient

client = AsyncAIssociateClient(
    api_key="<AISSOCIATE_API_KEY>",
)
```

Note that instead of explicitly setting the API Key, we recommend setting the ```AISSOCIATE_API_KEY``` as an environment 
variable either by exporting it ```export AISSOCIATE_API_KEY=<your-key>``` or by loading it from the ```.env``` file.

### Parameters
- `api_key`: Your API key for authentication.
- `base_url`: The base URL of the API server (The default is ```https://aissociate.at```).

## Usage

### Questions
Asking questions works be iterating over the event responses from the ```ask(question: str, legal_area: LegalArea = LegalArea.CIVIL)``` 
method of the client. 

```python
import asyncio
from aissociate import AsyncAIssociateClient, LegalArea


client = AsyncAIssociateClient()

async def main():
    stream = client.ask(
        "Fasse die Judikatur des OGH zur Mietzinsminderung in der Covid-Pandemie zusammen.",
        legal_area=LegalArea.CIVIL
    )
    async for event in stream:
        print(event.text, end="")


if __name__ == "__main__":
    asyncio.run(main())
```

Explicitly set the area of law by selecting one of the available knowledge databases (civil, criminal, public 
and tax law) and passing it via the legal area parameter, e.g.

```python
from aissociate import LegalArea

client.ask("Welche rechtlichen Bestimmungen gelten für die Untersuchungshaft in Österreich?", legal_area=LegalArea.CRIMINAL)
```

When selecting public law, our internal knowledge database is used and includes all federal laws. 
Optionally set the scope to also include austrian state laws (e.g. "Wien", "Tirol", "Steiermark", "Kärnten", 
"Niederösterreich", "Oberösterreich", "Vorarlberg", "Salzburg", "Burgenland").

All possible types are defined in ```models.shared```. If no legal area is selected, AI:ssociate without querying our 
knowledge databases, which can be helpful for general tasks like summarizing, translation, etc. 

```python
from aissociate import LegalArea, Scope

client.ask(
    "Ich will meine Hausfassade neu streichen – muss ich dafür eine Bewilligung einholen?", 
    legal_area=LegalArea.PUBLIC,
    scope="Tirol"
)
```

### Conversations
To ask follow-up questions within the same conversation context, use a session object. 
This ensures that the same ```thread_id``` is reused for each message, allowing the backend to 
maintain conversational memory.

```python
import asyncio
from aissociate import AsyncAIssociateClient


client = AsyncAIssociateClient()

async def main():
    async with client.session() as chat:
        async for event in chat.ask("Was ist Schadenersatz?"):
            print(event.text, end="")

        async for event in chat.ask("Fasse das stichpunktartig zusammen."):
            print(event.text, end="")

if __name__ == "__main__":
    asyncio.run(main())
```

You can still use ```client.ask(...)``` directly for single-turn, stateless queries.

### Resetting the thread
If you want to start a new conversation within the same session, simply call ```chat.reset()``` to clear the 
previous thread ID:

```python
import asyncio
from aissociate import AsyncAIssociateClient


client = AsyncAIssociateClient()

async def main():
    async with client.session() as chat:
        async for event in chat.ask("Was ist Schadenersatz?"):
            print(event.text, end="")

        chat.reset()

        async for event in chat.ask("Was ist ein Vertrag?"):
            print(event.text, end="")

if __name__ == "__main__":
    asyncio.run(main())
```

### Encryption with Passphrases

AI:ssociate supports **end-to-end encryption** for your messages and files via a user-chosen passphrase.

* If you provide a `passphrase`, all message and file content will be encrypted.
* If you don’t set a passphrase, a secure **system-managed passphrase** will be used instead.
* The `passphrase` is included as an HTTP header with each request to identify and unlock the corresponding encrypted data.

#### Example Usage

```python
from aissociate import AsyncAIssociateClient

client = AsyncAIssociateClient(passphrase="correct-horse-battery-staple")
```

All requests made with this client instance will now include the passphrase and interact with encrypted data accordingly.

#### Security Note

* **Keep your passphrase safe** — AI\:ssociate cannot recover encrypted content without it.
* Changing the passphrase later will prevent access to previously encrypted messages unless the original passphrase is reused.

### Files
The AI:ssociate client supports uploading files to provide context for your legal questions. Files are processed
asynchronously in the backend and may not be available immediately after upload.

#### Upload Method Parameters
The upload() method supports the following parameters:

| Parameter           | Type                 | Default   | Description                          |
|---------------------|----------------------|-----------|--------------------------------------|
| `files`             | `str` \| `list[str]` | Required  | File path(s) to upload               |
| `wait_for_completion` | `bool`             | `False`   | Wait until all uploads are processed |
| `progress_callback` | `callable`           | `None`    | Function called with progress updates|
| `poll_interval`     | `float`              | `1.0`     | Seconds between status checks        |
| `timeout`           | `float`              | `None`    | Maximum wait time in seconds         |


The progress callback receives three parameters:

- filename (str): Name of the file being processed
- progress (int): Progress percentage (0-100)
- status (str): Current status ("PENDING", "PROGRESS", "SUCCESS", "FAILURE")

```python
def my_progress_handler(filename, progress, status):
    if status == "PROGRESS":
        print(f"Processing {filename}: {progress}%")
    elif status == "SUCCESS":
        print(f"{filename} completed")
    elif status == "FAILURE":
        print(f"{filename} failed")
```

#### Basic File Upload
Upload files and get task information immediately:


```python
import asyncio
from aissociate import AsyncAIssociateClient

async def basic_upload():
    async with AsyncAIssociateClient() as client:
        # Upload files - returns immediately with task IDs
        files = ["contract.pdf", "legal_document.docx"]
        response = await client.upload(files)

        print("Upload initiated:")
        for task in response["tasks"]:
            print(f"\tTask ID: {task['task_id']}")
            print(f"\tFilename: {task['filename']}")

asyncio.run(basic_upload())
```

#### Upload and Wait for Completion

For simpler workflows, you can wait for uploads to complete before proceeding:

```python
async def upload_and_wait():
    async with AsyncAIssociateClient() as client:
        # Upload files and wait until they're fully processed
        files = ["document.pdf"]
        response = await client.upload(files, wait_for_completion=True)

        print("All files uploaded and processed!")
        for task in response["tasks"]:
            if task.get("file"):
                print(f"File ready: {task['file']['filename']} (UUID: {task['file']['uuid']})")

asyncio.run(upload_and_wait())
```


#### Upload with Progress Tracking

Monitor upload progress in real-time with a progress callback:


```python
async def upload_with_progress():
    async with AsyncAIssociateClient() as client:
        def progress_callback(filename, progress, status):
            print(f"{filename}: {progress}% ({status})")

        response = await client.upload(
            ["large_document.pdf"],
            wait_for_completion=True,
            progress_callback=progress_callback,
            poll_interval=0.5,  # Check every 500ms
            timeout=300  # 5 minute timeout
        )

        print("Upload completed!")

asyncio.run(upload_with_progress())
```

#### Session-based Upload

Upload files within a session context:


```python
async def session_upload():
    async with AsyncAIssociateClient() as client:
        async with client.session() as chat:
            # Upload and wait for completion in session
            await chat.upload(
                ["contract.pdf"],
                wait_for_completion=True
            )

            # Now use the file in queries
            async for event in chat.ask(
                "Fasse diesen Vertrag zusammen.",
                files=["contract.pdf"]
            ):
                print(event.text, end="")

asyncio.run(session_upload())
```


## Notes

The provided API key in the script is for demonstration purposes and should be replaced with a valid key.
Ensure that your API key remains confidential and is not shared publicly.

## Troubleshooting

If you receive a 401 Unauthorized response, ensure your API key is correct and active.
If the request times out, check your internet connection and API availability.